Erlang records with both type and value restrictions as well as default values
Asked Answered
C

2

5

I am trying to write a record which represents a bank account:

-record(account, {  name :: atom(),
                    type :: atom(),
                    balance = 0 :: integer()  }).

I also want to restrict the balance to always be >= 0. How do I do this?

Clearance answered 5/4, 2013 at 18:19 Comment(3)
Note that while you can declare the type to be a non-negative integer this does not enforce anything at run-time, it is really only a comment. You can set the balance field to any value you choose and any type.Impending
@rviding How can I enforce this behavior at runtime (without function guards)? Or does this pseudo-static typing just not exist in Erlang?Clearance
You can't! Erlang is too dynamic, so static typing does not exist in the language and the compiler does not use this information. It is only for documentation and for the type checking tool dialyzer.Impending
G
6

Something like balance = 0 :: 0 | pos_integer() might do the trick.

edit wasn't sure it existed, but non_neg_integer() would be better :

balance = 0 :: non_neg_integer()
Gustav answered 5/4, 2013 at 18:49 Comment(3)
I wasn't aware that existed either. Thanks a ton!Clearance
Beware that as said Robert Virding this is only information for the reader, documentation or tools. It doesn't prevent balance to be negative or anything else (like an atom). If it is important in your code you must use guard like is_integer(B) andalso B >= 0 before using it and/or modifying a balance.Adaptive
Thanks @Pascal, I'll be sure to use those guards. :)Clearance
D
7

As noted by others, the type specifications are merely inputs to analysis tools like PropEr and Dialyzer. If you need to enforce the invariant balance >= 0, the account type should be encapsulated, accessible only to functions that respect the invariant:

-module(account).

-record(account, { name :: atom(),
                   type :: atom(),
                   balance = 0 :: non_neg_integer() }).

%% Declares a type whose structure should not be visible externally.
-opaque account() :: #account{}.
%% Exports the type, making it available to other modules as 'account:account()'.
-export_type([account/0]).

%% Account constructor. Used by other modules to create accounts.
-spec new(atom(), atom(), non_neg_integer()) -> account().
new(Name, Type, InitialBalance) ->
    A = #account{name=Name, type=Type},
    set_balance(A, InitialBalance).

%% Safe setter - checks the balance invariant
-spec set_balance(account(), non_neg_integer()) -> account().
set_balance(Account, Balance) when is_integer(Balance) andalso Balance >= 0 ->
    Account#account{balance=Balance};
set_balance(_, _) -> error(badarg). % Bad balance

Notice how this resembles a class with private fields in object-oriented languages like Java or C++. By restricting access to "trusted" constructors and accessors, the invariant is enforced.

This solution doesn't provide protection against malicious modification of the balance field. It's entirely possible for code in another module to ignore the "opaque" type specification and replace the balance field in the record (since records are just tuples).

Diastyle answered 8/4, 2013 at 15:24 Comment(1)
Thanks for the example and the warning!Clearance
G
6

Something like balance = 0 :: 0 | pos_integer() might do the trick.

edit wasn't sure it existed, but non_neg_integer() would be better :

balance = 0 :: non_neg_integer()
Gustav answered 5/4, 2013 at 18:49 Comment(3)
I wasn't aware that existed either. Thanks a ton!Clearance
Beware that as said Robert Virding this is only information for the reader, documentation or tools. It doesn't prevent balance to be negative or anything else (like an atom). If it is important in your code you must use guard like is_integer(B) andalso B >= 0 before using it and/or modifying a balance.Adaptive
Thanks @Pascal, I'll be sure to use those guards. :)Clearance

© 2022 - 2024 — McMap. All rights reserved.