I am working on a math library for an online game. I want to prevent as many errors as possible and I want to catch all errors as early as possible to simplify debugging in the long run. However, I feel like I drown in details. This is quite different from "let it crash" motto.
point
has {number(), number()}
type or better say {0, 0}
is a point. I started with Dialyzer
specs and guard statements which work as a poor man's assertions.
Here is my point
class.
-module(point).
-author("nt").
-export([is_point/1, distance/2, translate/2, pointToMap/1]).
-type point() :: {number(), number()}.
-export_type([point/0]).
is_point({X, Y}) when is_number(X), is_number(Y) -> true;
is_point(_) -> false.
distance({X1, Y1} = A, {X2, Y2} = B) ->
case is_point(A) of false -> error(badarg); _ -> ok end,
case is_point(B) of false -> error(badarg); _ -> ok end,
math:sqrt(math:pow((X2 - X1), 2) + math:pow(Y2 - Y1, 2)).
translate({X1, Y1} = A, {X2, Y2} = B) ->
case is_point(A) of false -> error(badarg); _ -> ok end,
case is_point(B) of false -> error(badarg); _ -> ok end,
{X1 + X2, Y1 + Y2}.
pointToMap({X, Y} = A) ->
case is_point(A) of false -> error(badarg); _ -> ok end,
#{
x => X,
y => Y
}.
%% Spec
-spec is_point(P :: point()) -> boolean().
-spec distance(A :: point(), B :: point()) -> float().
-spec translate(A :: point(), B :: point()) -> point().
-spec pointToMap(A :: point()) -> #{x:= number(), y := number()}.
{{0, 0}, {0, 0}}
is a rect
.
rect
"class" relies on point
.
-module(rect).
-author("nt").
%% API
-export([is_rect/1, contains/2]).
-type rect() :: {point:point(), point:point()}.
-export_type([rect/0]).
is_rect({{OriginX, OriginY}, {W, H}}) when is_number(OriginX), is_number(OriginY), is_number(W), is_number(H) -> true;
is_rect(_) -> false.
-spec contains(Rect, Point) -> boolean() when Rect :: rect(), Point :: point:point().
contains({{OriginX, OriginY}, {W, H}} = R, {X, Y} = P) ->
case is_rect(R) of false -> error(badarg); _ -> ok end,
case point:is_point(P) of false -> error(badarg); _ -> ok end,
((OriginX < X) and (X < (OriginX + W))) and ((OriginY < Y) and (Y < (OriginY + H))).
Things are getting really massive and daunting. Given the fact there is almost no business logic I wonder how fast codebase will become a complete mess if I continue to check all the required preconditions in all functions.
Update:
I think about removing all the specs and "assertions" from the code. Functions like pointToMap
will be also removed as I don't need to rely on concrete types anymore. Something like tupleToMap
should be used instead.
point.erl
:
-module(point).
-author("nt").
-export([distance/2, translate/2]).
distance({X1, Y1}, {X2, Y2}) ->
math:sqrt(math:pow((X2 - X1), 2) + math:pow(Y2 - Y1, 2)).
translate({X1, Y1}, {X2, Y2}) ->
{X1 + X2, Y1 + Y2}.
rect.erl
-module(rect).
-author("nt").
-export([contains/2]).
contains({{OriginX, OriginY}, {W, H}}, {X, Y}) ->
((OriginX < X) and (X < (OriginX + W))) and ((OriginY < Y) and (Y < (OriginY + H))).