├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── cover.spec ├── include └── hope_kv_list.hrl ├── rebar_test_build.config ├── src ├── hope.app.src ├── hope_fun.erl ├── hope_gen_dictionary.erl ├── hope_gen_monad.erl ├── hope_kv_list.erl ├── hope_list.erl ├── hope_option.erl ├── hope_result.erl └── hope_time.erl └── test ├── hope_dictionary_SUITE.erl ├── hope_fun_SUITE.erl ├── hope_kv_list_SUITE.erl ├── hope_list_SUITE.erl ├── hope_option_SUITE.erl └── hope_result_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | ebin/ 2 | logs/ 3 | test/*.beam 4 | /deps/proper/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | recipients: 3 | - siraaj@khandkar.net 4 | 5 | language: 6 | erlang 7 | 8 | otp_release: 9 | - 22.1 10 | - 22.0 11 | - 21.3 12 | - 21.2 13 | - 21.1 14 | - 21.0 15 | - 20.3 16 | - 19.3 17 | - 18.3 18 | - 18.1 19 | - 18.0 20 | - 17.5 21 | - 17.0 22 | 23 | script: 24 | "make travis_ci" 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR := rebar --config ./rebar_test_build.config 2 | 3 | .PHONY: \ 4 | travis_ci \ 5 | fresh-build \ 6 | compile \ 7 | clean \ 8 | deps \ 9 | deps-get \ 10 | deps-update \ 11 | dialyze \ 12 | test 13 | 14 | all: \ 15 | clean \ 16 | deps \ 17 | compile \ 18 | test \ 19 | dialyze 20 | 21 | travis_ci: \ 22 | deps \ 23 | compile \ 24 | test 25 | 26 | fresh-build: \ 27 | clean \ 28 | compile 29 | 30 | compile: 31 | @$(REBAR) compile 32 | 33 | clean: 34 | @$(REBAR) clean 35 | 36 | deps: \ 37 | deps-get \ 38 | deps-update 39 | 40 | deps-get: 41 | @$(REBAR) get-deps 42 | 43 | deps-update: 44 | @$(REBAR) update-deps 45 | 46 | dialyze: 47 | @dialyzer ebin 48 | 49 | test: 50 | @$(REBAR) ct skip_deps=true --verbose=0 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/xandkar/hope.svg?branch=master)](https://travis-ci.org/xandkar/hope) 2 | 3 | Hope 4 | ==== 5 | 6 | A quest for a "standard" library with uniform, composable abstractions. 7 | 8 | Originally motivated by a desire for an error monad and generic option type 9 | operations, and stood for _Higher Order Programming in Erlang_. Soon after, I 10 | wished all standard containers used consistent conventions and protocols (such 11 | as consistent accessor names, argument positioning rules and expression of 12 | semantics with option and result types). 13 | 14 | Here lies an experiment to see what something like that could look like. As all 15 | proper experiments should, this one is used daily in production projects (hence 16 | the high-ish version number, 'cause semver). 17 | 18 | 19 | Conventions 20 | ----------- 21 | 22 | I entertain any forward-thinking library design ideas, but more than anything 23 | else, these are influenced by Jane Street's Core of the OCaml world. 24 | 25 | - A module per data type implementation 26 | - Name of the module is the name of the type 27 | - Inside the module, the type it implements is always named t(..), such as: 28 | `hope_foo:t()`, _not_ `hope_foo:foo()` 29 | - t(..) is always the first argument 30 | - Names of private records _may_ be short, such as: `#foo{}` or `#t{}` (Though 31 | I'm second-guessing this idea, since seeing `{t, ..}` in stack traces is less 32 | than helpful. I'm considering requiring fully-qualified names for all record 33 | definitions and maybe short-handing what would've been `#t{..}` as 34 | `-define(T, ?MODULE). -record(?T, {..}).`, which may be a bit ugly. Still 35 | thinking...) 36 | - Names of public records _must_ be fully qualified, such as: `#hope_module_record{}` 37 | - Names of all modules _must_ be fully qualified, such as: `hope_module` (this 38 | should go without saying, but just to be sure...) 39 | - Keep the number of (anonymous) arguments "reasonably" low: 40 | + up to 3 is normal 41 | + 4 is suspicious but may be reasonable 42 | + 5 is _very_ suspicious and probably unnecessary 43 | + more than 5 is unacceptable, so consider reducing by: 44 | 1. revising abstractions, or, if not practical 45 | 2. creating a public record specifically for the purpose of passing 46 | many arguents, which simulates labeled arguments. For an example see 47 | https://github.com/xandkar/oauth1_core where I used that technique 48 | extensively (especially in oauth1_server.erl) 49 | 50 | 51 | Abstractions 52 | ------------ 53 | 54 | ### Monads 55 | 56 | A class of burritos, used for expressing sequences of operations on some data 57 | type. Defined in `hope_gen_monad`, implemented as: 58 | 59 | - `hope_result`: for composition of common functions returning 60 | `{ok, Val} | {error, Reason}`. An alternative to exceptions, which makes the 61 | error conditions apparent in the spec/signature. Analogous to Haskell's 62 | `Data.Either a b`, Jane Street Core's (OCaml) `('a, 'b) Result.t`, Rust's 63 | `Result` 64 | - `hope_option`: for expressing and composing the intention that the value may 65 | or may not be available. An alternative to the common `undefined` (which is 66 | equivalent to the dreaded `null`). Analogous to ML's (SML, OCaml, etc) 67 | `'a Option.t`, Rust's `Option` and Haskell's `Data.Maybe a` [1]. 68 | 69 | 70 | ### Containers 71 | 72 | A class of abstract data types to which we have exclusive access and can put 73 | things in and take them out. See issue #9 74 | 75 | - Operations on all abstract types of containers _should_ share a common lexicon 76 | - Concrete implementations of an abstract data type _must_ be swapable 77 | 78 | #### Dictionary 79 | 80 | Defined in `hope_gen_dictionary`, implemented as: 81 | 82 | - `hope_kv_list`. Equivalent to orddict/proplist. Operations implemented with 83 | BIFs from `lists` module, where possible 84 | 85 | TBD: 86 | - `hope_hash_tbl`. API around stdlib's `dict` 87 | - `hope_gb_dict`. API around stdlib's `gb_trees` 88 | 89 | #### Set 90 | 91 | TBD: 92 | - `hope_hash_set`. API around stdlib's `sets` 93 | - `hope_gb_set`. API around stdlib's `gb_sets` 94 | 95 | #### Queue 96 | 97 | TBD 98 | 99 | Should include both FIFO (queue) and LIFO (stack), so that user can swap if a 100 | different order is desired. 101 | 102 | Should we attempt to include priority queues or make them a separate abstract 103 | type? 104 | 105 | #### Sequence 106 | 107 | TBD 108 | 109 | Not yet defined and only partially implemented as: 110 | 111 | - `hope_list` 112 | 113 | 114 | ### Resources 115 | 116 | A class of abstract systems to which we share access with an unknown number of 117 | users and can make requests to perform operations which may not get done for 118 | any number of reasons. 119 | 120 | #### Storage 121 | 122 | TBD 123 | 124 | See issue #11 125 | 126 | 127 | [1]: http://en.wikipedia.org/wiki/Option_type 128 | -------------------------------------------------------------------------------- /cover.spec: -------------------------------------------------------------------------------- 1 | {incl_app, hope, details}. 2 | -------------------------------------------------------------------------------- /include/hope_kv_list.hrl: -------------------------------------------------------------------------------- 1 | -record(hope_kv_list_presence_violations, 2 | { keys_missing :: [A] 3 | , keys_duplicated :: [A] 4 | , keys_unsupported :: [A] 5 | }). 6 | -------------------------------------------------------------------------------- /rebar_test_build.config: -------------------------------------------------------------------------------- 1 | %%% -*- mode: erlang -*- 2 | {clean_files, ["test/*.beam"]}. 3 | 4 | {deps, 5 | [{proper, ".*", {git, "https://github.com/manopapad/proper.git", "ced2ddb3a719aec22e5abe55c51b56658b0e8a87"}}] 6 | }. 7 | 8 | {cover_enabled, true}. 9 | 10 | {clean_files, ["test/*.beam"]}. 11 | -------------------------------------------------------------------------------- /src/hope.app.src: -------------------------------------------------------------------------------- 1 | {application, hope, 2 | [ 3 | {description, "Higher Order Programming in Erlang"}, 4 | {vsn, "4.1.0"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {env, []} 11 | ]}. 12 | -------------------------------------------------------------------------------- /src/hope_fun.erl: -------------------------------------------------------------------------------- 1 | -module(hope_fun). 2 | 3 | -export( 4 | [ id/1 5 | , curry/1 6 | , compose/1 % alias for compose_right/1 7 | , compose_right/1 8 | , compose_left/1 9 | , thread/2 10 | ]). 11 | 12 | -spec id(A) -> 13 | A. 14 | id(X) -> 15 | X. 16 | 17 | -spec curry(fun()) -> 18 | fun(). 19 | curry(F) -> 20 | {arity, Arity} = erlang:fun_info(F, arity), 21 | curry(F, [], Arity). 22 | 23 | -spec curry(fun(), list(), integer()) -> 24 | fun(). 25 | curry(F, Args, 0) -> 26 | apply(F, lists:reverse(Args)); 27 | curry(F, Args, Arity) -> 28 | fun (X) -> curry(F, [X | Args], Arity - 1) end. 29 | 30 | -spec compose([fun((A) -> B)]) -> 31 | fun((A) -> B). 32 | compose(Fs) -> 33 | compose_right(Fs). 34 | 35 | -spec compose_right([fun((A) -> B)]) -> 36 | fun((A) -> B). 37 | compose_right(Fs) -> 38 | compose_given_fold(Fs, fun lists:foldr/3). 39 | 40 | -spec compose_left([fun((A) -> B)]) -> 41 | fun((A) -> B). 42 | compose_left(Fs) -> 43 | compose_given_fold(Fs, fun lists:foldl/3). 44 | 45 | -spec thread([fun((A) -> B)], A) -> 46 | B. 47 | thread(Fs, X) -> 48 | F = compose_left(Fs), 49 | F(X). 50 | 51 | 52 | %% ============================================================================ 53 | 54 | -spec compose_given_fold([fun((A) -> B)], fun((fun((A, B) -> C), B, [A]) -> C)) -> 55 | fun((A) -> C). 56 | compose_given_fold(Fs, Fold) -> 57 | fun (X) -> Fold(fun (F, Y) -> F(Y) end, X, Fs) end. 58 | -------------------------------------------------------------------------------- /src/hope_gen_dictionary.erl: -------------------------------------------------------------------------------- 1 | -module(hope_gen_dictionary). 2 | 3 | -export_type( 4 | [ t/2 5 | ]). 6 | 7 | 8 | -type t(_Key, _Value) :: 9 | term(). 10 | 11 | 12 | -callback empty() -> 13 | t(_K, _V). 14 | 15 | -callback get(t(K, V), K) -> 16 | hope_option:t(V). 17 | 18 | -callback get(t(K, V), K, V) -> 19 | V. 20 | 21 | -callback get(t(K, V), K, V, fun((V) -> boolean())) -> 22 | V. 23 | 24 | -callback set(t(K, V), K, V) -> 25 | t(K, V). 26 | 27 | -callback update(t(K, V), K, fun((hope_option:t(V)) -> V)) -> 28 | t(K, V). 29 | 30 | -callback pop(t(K, V), K) -> 31 | {hope_option:t(V), t(K, V)}. 32 | 33 | -callback map(t(K, V), fun((K, V) -> V)) -> 34 | t(K, V). 35 | 36 | -callback filter(t(K, V), fun((K, V) -> boolean())) -> 37 | t(K, V). 38 | 39 | -callback fold(t(K, V), fun((K, V, Acc) -> Acc), Acc) -> 40 | Acc. 41 | 42 | -callback iter(t(K, V), fun((K, V) -> any())) -> 43 | {}. 44 | 45 | %% TODO: Decide if validation is to be done. If yes - wrap in hope_result:t/1 46 | -callback of_kv_list([{K, V}]) -> 47 | t(K, V). 48 | 49 | -callback to_kv_list(t(K, V)) -> 50 | [{K, V}]. 51 | 52 | -callback has_key(t(K, _), K) -> 53 | boolean(). 54 | -------------------------------------------------------------------------------- /src/hope_gen_monad.erl: -------------------------------------------------------------------------------- 1 | -module(hope_gen_monad). 2 | 3 | -type t(_A) :: 4 | term(). 5 | 6 | -callback return(A) -> 7 | t(A). 8 | 9 | -callback map(t(A), fun((A) -> (B))) -> 10 | t(B). 11 | 12 | %% @doc "pipe" is equivalent to traditional "bind", in general use-case, but is 13 | %% arguably more useful for composition in Erlang's syntactic setting. 14 | %% @end 15 | -callback pipe([fun((A) -> t(B))], A) -> 16 | t(B). 17 | -------------------------------------------------------------------------------- /src/hope_kv_list.erl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------------- 2 | %%% Equivalent to stdlib's orddict, but with a pretty (IMO), uniform interface. 3 | %%%---------------------------------------------------------------------------- 4 | -module(hope_kv_list). 5 | 6 | -include_lib("hope_kv_list.hrl"). 7 | 8 | -behavior(hope_gen_dictionary). 9 | 10 | -export_type( 11 | [ t/2 12 | ]). 13 | 14 | -export( 15 | [ empty/0 16 | , get/2 % get option 17 | , get/3 % get existing or default 18 | , get/4 % get existing if valid, or default 19 | , set/3 20 | , update/3 21 | , pop/2 22 | , iter/2 23 | , map/2 24 | , filter/2 25 | , fold/3 26 | , of_kv_list/1 27 | , to_kv_list/1 28 | , has_key/2 29 | , find_unique_presence_violations/2 % No optional keys 30 | , find_unique_presence_violations/3 % Specify optional keys 31 | , validate_unique_presence/2 % No optional keys 32 | , validate_unique_presence/3 % Specify optional keys 33 | , presence_violations_to_list/1 34 | ]). 35 | 36 | 37 | -type t(K, V) :: 38 | [{K, V}]. 39 | 40 | -type presence_violations(A) :: 41 | % This is a hack to effectively parametarize the types of record fields. 42 | % IMPORTANT: Make sure that the order of fields matches the definition of 43 | % #hope_kv_list_presence_violations 44 | { hope_kv_list_presence_violations 45 | , [A] % keys_missing 46 | , [A] % keys_duplicated 47 | , [A] % keys_unsupported 48 | }. 49 | 50 | -type presence_error(A) :: 51 | {keys_missing , [A]} 52 | | {keys_duplicated , [A]} 53 | | {keys_unsupported , [A]} 54 | . 55 | 56 | 57 | %% ============================================================================ 58 | %% API 59 | %% ============================================================================ 60 | 61 | -spec empty() -> 62 | []. 63 | empty() -> 64 | []. 65 | 66 | -spec get(t(K, V), K) -> 67 | hope_option:t(V). 68 | get(T, K) -> 69 | case lists:keyfind(K, 1, T) 70 | of false -> none 71 | ; {K, V} -> {some, V} 72 | end. 73 | 74 | -spec get(t(K, V), K, V) -> 75 | V. 76 | get(T, K, Default) -> 77 | Vopt = get(T, K), 78 | hope_option:get(Vopt, Default). 79 | 80 | -spec get(t(K, V), K, V, fun((V) -> boolean())) -> 81 | V. 82 | get(T, K, Default, IsValid) -> 83 | VOpt1 = get(T, K), 84 | VOpt2 = hope_option:validate(VOpt1, IsValid), 85 | hope_option:get(VOpt2, Default). 86 | 87 | -spec set(t(K, V), K, V) -> 88 | t(K, V). 89 | set(T, K, V) -> 90 | lists:keystore(K, 1, T, {K, V}). 91 | 92 | -spec update(t(K, V), K, fun((hope_option:t(V)) -> V)) -> 93 | t(K, V). 94 | update(T, K, F) -> 95 | V1Opt = get(T, K), 96 | V2 = F(V1Opt), 97 | % TODO: Eliminate the 2nd lookup. 98 | set(T, K, V2). 99 | 100 | -spec pop(t(K, V), K) -> 101 | {hope_option:t(V), t(K, V)}. 102 | pop(T1, K) -> 103 | case lists:keytake(K, 1, T1) 104 | of {value, {K, V}, T2} -> {{some, V}, T2} 105 | ; false -> {none , T1} 106 | end. 107 | 108 | -spec iter(t(K, V), fun((K, V) -> any())) -> 109 | {}. 110 | iter(T, F1) -> 111 | F2 = lift_map(F1), 112 | ok = lists:foreach(F2, T), 113 | {}. 114 | 115 | -spec map(t(K, V), fun((K, V) -> V)) -> 116 | t(K, V). 117 | map(T, F1) -> 118 | F2 = fun ({K, _}=X) -> {K, apply_map(F1, X)} end, 119 | lists:map(F2, T). 120 | 121 | -spec filter(t(K, V), fun((K, V) -> boolean())) -> 122 | t(K, V). 123 | filter(T, F1) -> 124 | F2 = lift_map(F1), 125 | lists:filter(F2, T). 126 | 127 | -spec fold(t(K, V), fun((K, V, Acc) -> Acc), Acc) -> 128 | Acc. 129 | fold(T, F1, Accumulator) -> 130 | F2 = fun ({K, V}, Acc) -> F1(K, V, Acc) end, 131 | lists:foldl(F2, Accumulator, T). 132 | 133 | -spec to_kv_list(t(K, V)) -> 134 | [{K, V}]. 135 | to_kv_list(T) -> 136 | T. 137 | 138 | -spec of_kv_list([{K, V}]) -> 139 | t(K, V). 140 | of_kv_list(List) -> 141 | % TODO: Decide if validation is to be done here. Do so if yes. 142 | List. 143 | 144 | -spec validate_unique_presence(T, [K]) -> 145 | hope_result:t(T, [presence_error(K)]) 146 | when T :: t(K, _V). 147 | validate_unique_presence(T, KeysRequired) -> 148 | KeysOptional = [], 149 | validate_unique_presence(T, KeysRequired, KeysOptional). 150 | 151 | -spec validate_unique_presence(t(K, _V), [K], [K]) -> 152 | hope_result:t(T, [presence_error(K)]) 153 | when T :: t(K, _V). 154 | validate_unique_presence(T, KeysRequired, KeysOptional) -> 155 | case find_unique_presence_violations(T, KeysRequired, KeysOptional) 156 | of #hope_kv_list_presence_violations 157 | { keys_missing = [] 158 | , keys_duplicated = [] 159 | , keys_unsupported = [] 160 | } -> 161 | {ok, T} 162 | ; #hope_kv_list_presence_violations{}=Violations -> 163 | {error, presence_violations_to_list(Violations)} 164 | end. 165 | 166 | -spec find_unique_presence_violations(t(K, _V), [K]) -> 167 | presence_violations(K). 168 | find_unique_presence_violations(T, KeysRequired) -> 169 | KeysOptional = [], 170 | find_unique_presence_violations(T, KeysRequired, KeysOptional). 171 | 172 | -spec find_unique_presence_violations(t(K, _V), [K], [K]) -> 173 | presence_violations(K). 174 | find_unique_presence_violations(T, KeysRequired, KeysOptional) -> 175 | KeysSupported = KeysRequired ++ KeysOptional, 176 | KeysGiven = [K || {K, _V} <- T], 177 | KeysGivenUnique = lists:usort(KeysGiven), 178 | KeysDuplicated = lists:usort(KeysGiven -- KeysGivenUnique), 179 | KeysMissing = KeysRequired -- KeysGivenUnique, 180 | KeysUnsupported = KeysGivenUnique -- KeysSupported, 181 | #hope_kv_list_presence_violations 182 | { keys_missing = KeysMissing 183 | , keys_duplicated = KeysDuplicated 184 | , keys_unsupported = KeysUnsupported 185 | }. 186 | 187 | -spec presence_violations_to_list(presence_violations(K)) -> 188 | [presence_error(K)]. 189 | presence_violations_to_list(#hope_kv_list_presence_violations 190 | { keys_missing = KeysMissing 191 | , keys_duplicated = KeysDuplicated 192 | , keys_unsupported = KeysUnsupported 193 | }) -> 194 | ErrorMissing = 195 | case KeysMissing 196 | of [] -> [] 197 | ; [_|_] -> [{keys_missing, KeysMissing}] 198 | end, 199 | ErrorDups = 200 | case KeysDuplicated 201 | of [] -> [] 202 | ; [_|_] -> [{keys_duplicated, KeysDuplicated}] 203 | end, 204 | ErrorUnsupported = 205 | case KeysUnsupported 206 | of [] -> [] 207 | ; [_|_] -> [{keys_unsupported, KeysUnsupported}] 208 | end, 209 | ErrorDups ++ ErrorMissing ++ ErrorUnsupported. 210 | 211 | -spec has_key(t(K, _), K) -> 212 | boolean(). 213 | has_key(T, K1) -> 214 | lists:any(fun ({K2, _}) -> K1 =:= K2 end, T). 215 | 216 | %% ============================================================================ 217 | %% Helpers 218 | %% ============================================================================ 219 | 220 | -spec lift_map(F) -> 221 | G 222 | when F :: fun(( K, V1 ) -> V2) 223 | , G :: fun(({K, V1}) -> V2) 224 | . 225 | lift_map(F) -> 226 | fun (X) -> apply_map(F, X) end. 227 | 228 | -spec apply_map(fun((K, V1) -> V2), {K, V1}) -> 229 | V2. 230 | apply_map(F, {K, V}) -> 231 | F(K, V). 232 | -------------------------------------------------------------------------------- /src/hope_list.erl: -------------------------------------------------------------------------------- 1 | -module(hope_list). 2 | 3 | -export_type( 4 | [ t/1 5 | ]). 6 | 7 | -export( 8 | [ unique_preserve_order/1 9 | , map/2 10 | , map/3 % Tunable recursion limit 11 | , map_rev/2 12 | , map_slow/2 13 | , map_result/2 % Not tail-recursive 14 | , first_match/2 15 | , divide/2 16 | ]). 17 | 18 | -define(DEFAULT_RECURSION_LIMIT, 1000). 19 | 20 | -type t(A) :: 21 | [A]. 22 | 23 | %% @doc Tail-recursive equivalent of lists:map/2 24 | %% @end 25 | -spec map([A], fun((A) -> (B))) -> 26 | [B]. 27 | map(Xs, F) -> 28 | map(Xs, F, ?DEFAULT_RECURSION_LIMIT). 29 | 30 | -spec map([A], fun((A) -> (B)), RecursionLimit :: non_neg_integer()) -> 31 | [B]. 32 | map(Xs, F, RecursionLimit) -> 33 | map(Xs, F, RecursionLimit, 0). 34 | 35 | map([], _, _, _) -> 36 | []; 37 | map([X1], F, _, _) -> 38 | Y1 = F(X1), 39 | [Y1]; 40 | map([X1, X2], F, _, _) -> 41 | Y1 = F(X1), 42 | Y2 = F(X2), 43 | [Y1, Y2]; 44 | map([X1, X2, X3], F, _, _) -> 45 | Y1 = F(X1), 46 | Y2 = F(X2), 47 | Y3 = F(X3), 48 | [Y1, Y2, Y3]; 49 | map([X1, X2, X3, X4], F, _, _) -> 50 | Y1 = F(X1), 51 | Y2 = F(X2), 52 | Y3 = F(X3), 53 | Y4 = F(X4), 54 | [Y1, Y2, Y3, Y4]; 55 | map([X1, X2, X3, X4, X5 | Xs], F, RecursionLimit, RecursionCount) -> 56 | Y1 = F(X1), 57 | Y2 = F(X2), 58 | Y3 = F(X3), 59 | Y4 = F(X4), 60 | Y5 = F(X5), 61 | Ys = 62 | case RecursionCount > RecursionLimit 63 | of true -> map_slow(Xs, F) 64 | ; false -> map (Xs, F, RecursionLimit, RecursionCount + 1) 65 | end, 66 | [Y1, Y2, Y3, Y4, Y5 | Ys]. 67 | 68 | %% @doc lists:reverse(map_rev(L, F)) 69 | %% @end 70 | -spec map_slow([A], fun((A) -> (B))) -> 71 | [B]. 72 | map_slow(Xs, F) -> 73 | lists:reverse(map_rev(Xs, F)). 74 | 75 | %% @doc Tail-recursive alternative to lists:map/2, which accumulates and 76 | %% returns list in reverse order. 77 | %% @end 78 | -spec map_rev([A], fun((A) -> (B))) -> 79 | [B]. 80 | map_rev(Xs, F) -> 81 | map_rev_acc(Xs, F, []). 82 | 83 | -spec map_rev_acc([A], fun((A) -> (B)), [B]) -> 84 | [B]. 85 | map_rev_acc([], _, Ys) -> 86 | Ys; 87 | map_rev_acc([X|Xs], F, Ys) -> 88 | Y = F(X), 89 | map_rev_acc(Xs, F, [Y|Ys]). 90 | 91 | -spec map_result([A], fun((A) -> (hope_result:t(B, C)))) -> 92 | hope_result:t([B], C). 93 | map_result([], _) -> 94 | {ok, []}; 95 | map_result([X | Xs], F) -> 96 | case F(X) 97 | of {ok, Y} -> 98 | case map_result(Xs, F) 99 | of {ok, Ys} -> 100 | {ok, [Y | Ys]} 101 | ; {error, _}=Error -> 102 | Error 103 | end 104 | ; {error, _}=Error -> 105 | Error 106 | end. 107 | 108 | -spec unique_preserve_order(t(A)) -> 109 | t(A). 110 | unique_preserve_order(L) -> 111 | PrependIfNew = 112 | fun (X, Xs) -> 113 | case lists:member(X, Xs) 114 | of true -> Xs 115 | ; false -> [X | Xs] 116 | end 117 | end, 118 | lists:reverse(lists:foldl(PrependIfNew, [], L)). 119 | 120 | -spec first_match([{Tag, fun((A) -> boolean())}], A) -> 121 | hope_option:t(Tag). 122 | first_match([], _) -> 123 | none; 124 | first_match([{Tag, F} | Tests], X) -> 125 | case F(X) 126 | of true -> {some, Tag} 127 | ; false -> first_match(Tests, X) 128 | end. 129 | 130 | %% @doc Divide list into sublists of up to a requested size + a remainder. 131 | %% Order unspecified. Size < 1 raises an error: 132 | %% `hope_list__divide__size_must_be_a_positive_integer' 133 | %% @end 134 | -spec divide([A], pos_integer()) -> 135 | [[A]]. 136 | divide(_, Size) when Size < 1 orelse not is_integer(Size) -> 137 | % Q: Why? 138 | % A: For N < 0, what does it mean to have a negative-sized chunk? 139 | % For N = 0, we can imagine that a single chunk is an empty list, but, 140 | % how many such chunks should we produce? 141 | % This is pretty-much equivalnet to the problem of deviding something by 0. 142 | error(hope_list__divide__size_must_be_a_positive_integer); 143 | divide([], _) -> 144 | []; 145 | divide([X1 | Xs], MaxChunkSize) -> 146 | MoveIntoChunks = 147 | fun (X2, {Chunk, Chunks, ChunkSize}) when ChunkSize >= MaxChunkSize -> 148 | {[X2], [Chunk | Chunks], 1} 149 | ; (X2, {Chunk, Chunks, ChunkSize}) -> 150 | {[X2 | Chunk], Chunks, ChunkSize + 1} 151 | end, 152 | {Chunk, Chunks, _} = lists:foldl(MoveIntoChunks, {[X1], [], 1}, Xs), 153 | [Chunk | Chunks]. 154 | -------------------------------------------------------------------------------- /src/hope_option.erl: -------------------------------------------------------------------------------- 1 | -module(hope_option). 2 | 3 | -behavior(hope_gen_monad). 4 | 5 | -export_type( 6 | [ t/1 7 | ]). 8 | 9 | -export( 10 | % Generic monad interface 11 | [ return/1 12 | , map/2 13 | , pipe/2 14 | 15 | % Specific to hope_option:t() 16 | , return/2 17 | , put/2 18 | , get/2 19 | , iter/2 20 | , of_result/1 21 | , of_undefined/1 22 | , to_undefined/1 23 | , validate/2 24 | ]). 25 | 26 | 27 | -type t(A) :: 28 | none 29 | | {some, A} 30 | . 31 | 32 | 33 | -spec put(A, fun((A) -> boolean())) -> 34 | t(A). 35 | put(X, F) -> 36 | return(X, F). 37 | 38 | -spec get(t(A), Default :: A) -> 39 | A. 40 | get({some, X}, _) -> X; 41 | get(none , Y) -> Y. 42 | 43 | -spec return(A) -> 44 | {some, A}. 45 | return(X) -> 46 | {some, X}. 47 | 48 | -spec return(A, fun((A) -> boolean())) -> 49 | t(A). 50 | return(X, Condition) -> 51 | case Condition(X) 52 | of true -> {some, X} 53 | ; false -> none 54 | end. 55 | 56 | -spec map(t(A), fun((A) -> (B))) -> 57 | t(B). 58 | map({some, X}, F) -> {some, F(X)}; 59 | map(none , _) -> none. 60 | 61 | -spec iter(t(A), fun((A) -> (any()))) -> 62 | {}. 63 | iter({some, X}, F) -> 64 | _ = F(X), 65 | {}; 66 | iter(none, _) -> 67 | {}. 68 | 69 | -spec pipe([fun((A) -> t(B))], A) -> 70 | t(B). 71 | pipe([], X) -> 72 | return(X); 73 | pipe([F|Fs], X) -> 74 | case F(X) 75 | of none -> none 76 | ; {some, Y} -> pipe(Fs, Y) 77 | end. 78 | 79 | -spec of_result(hope_result:t(A, _B)) -> 80 | t(A). 81 | of_result({ok, X}) -> {some, X}; 82 | of_result({error, _}) -> none. 83 | 84 | -spec of_undefined(undefined | A) -> 85 | t(A). 86 | of_undefined(undefined) -> none; 87 | of_undefined(X) -> {some, X}. 88 | 89 | -spec to_undefined(t(A)) -> 90 | undefined | A. 91 | to_undefined(none) -> undefined; 92 | to_undefined({some, X}) -> X. 93 | 94 | -spec validate(t(A), fun((A) -> boolean())) -> 95 | t(A). 96 | validate(none, _) -> 97 | none; 98 | validate({some, X}=T, F) -> 99 | case F(X) 100 | of false -> none 101 | ; true -> T 102 | end. 103 | -------------------------------------------------------------------------------- /src/hope_result.erl: -------------------------------------------------------------------------------- 1 | -module(hope_result). 2 | 3 | -behavior(hope_gen_monad). 4 | 5 | -export_type( 6 | [ t/2 7 | , exn_class/0 8 | , exn_value/1 9 | ]). 10 | 11 | -export( 12 | % Generic monad interface 13 | [ return/1 14 | , map/2 % map/2 is alias for map_ok/2 15 | , pipe/2 16 | 17 | % Specific to hope_result:t() 18 | , map_ok/2 19 | , map_error/2 20 | , tag_error/2 21 | , lift_exn/1 22 | , lift_exn/2 23 | , lift_map_exn/3 24 | ]). 25 | 26 | -type exn_class() :: 27 | error 28 | | exit 29 | | throw 30 | . 31 | 32 | -type exn_value(A) :: 33 | {exn_class(), A}. 34 | 35 | -type t(A, B) :: 36 | {ok, A} 37 | | {error, B} 38 | . 39 | 40 | 41 | -spec return(A) -> 42 | {ok, A}. 43 | return(X) -> 44 | {ok, X}. 45 | 46 | -spec map(t(A, Error), fun((A) -> (B))) -> 47 | t(B, Error). 48 | map({_, _}=T, F) -> 49 | map_ok(T, F). 50 | 51 | -spec map_ok(t(A, Error), fun((A) -> (B))) -> 52 | t(B, Error). 53 | map_ok({ok, X}, F) -> 54 | {ok, F(X)}; 55 | map_ok({error, _}=Error, _) -> 56 | Error. 57 | 58 | -spec map_error(t(A, B), fun((B) -> (C))) -> 59 | t(A, C). 60 | map_error({ok, _}=Ok, _) -> 61 | Ok; 62 | map_error({error, Reason}, F) -> 63 | {error, F(Reason)}. 64 | 65 | -spec tag_error(t(A, Reason), Tag) -> 66 | t(A, {Tag, Reason}). 67 | tag_error({ok, _}=Ok, _) -> 68 | Ok; 69 | tag_error({error, Reason}, Tag) -> 70 | {error, {Tag, Reason}}. 71 | 72 | -spec pipe([F], X) -> 73 | t(Ok, Error) 74 | when X :: any() 75 | , Ok :: any() 76 | , Error :: any() 77 | , F :: fun((X) -> t(Ok, Error)) 78 | . 79 | pipe([], X) -> 80 | {ok, X}; 81 | pipe([F|Fs], X) -> 82 | case F(X) 83 | of {error, _}=E -> E 84 | ; {ok, Y} -> pipe(Fs, Y) 85 | end. 86 | 87 | -spec lift_exn(F) -> G 88 | when F :: fun((A) -> B) 89 | , G :: fun((A) -> t(B, exn_value(any()))) 90 | . 91 | lift_exn(F) when is_function(F, 1) -> 92 | ID = fun hope_fun:id/1, 93 | lift_map_exn(F, ID, ID). 94 | 95 | -spec lift_exn(F, ErrorTag) -> G 96 | when F :: fun((A) -> B) 97 | , G :: fun((A) -> t(B, {ErrorTag, exn_value(any())})) 98 | . 99 | lift_exn(F, ErrorTag) when is_function(F, 1) -> 100 | ID = fun hope_fun:id/1, 101 | Tag = fun (Reason) -> {ErrorTag, Reason} end, 102 | lift_map_exn(F, ID, Tag). 103 | 104 | -spec lift_map_exn(F, MapOk, MapError) -> G 105 | when F :: fun((A) -> B) 106 | , MapOk :: fun((B) -> C) 107 | , MapError :: fun((exn_value(any())) -> Error) 108 | , G :: fun((A) -> t(C, Error)) 109 | . 110 | lift_map_exn(F, MapOk, MapError) when is_function(F, 1) -> 111 | fun(X) -> 112 | Result = 113 | try 114 | {ok, F(X)} 115 | catch Class:Reason -> 116 | {error, {Class, Reason}} 117 | end, 118 | % Applying maps separately as to not unintentionally catch an exception 119 | % raised in a map. 120 | case Result 121 | of {ok , _}=Ok -> map_ok (Ok , MapOk) 122 | ; {error, _}=Error -> map_error(Error, MapError) 123 | end 124 | end. 125 | -------------------------------------------------------------------------------- /src/hope_time.erl: -------------------------------------------------------------------------------- 1 | -module(hope_time). 2 | 3 | -export_type( 4 | [ t/0 5 | ]). 6 | 7 | -export( 8 | [ now/0 9 | , of_timestamp/1 10 | , to_unix_time/1 11 | , of_iso8601/1 12 | 13 | % Floatable 14 | , of_float/1 15 | , to_float/1 16 | 17 | % TODO: Stringable 18 | ]). 19 | 20 | 21 | -define(T, #?MODULE). 22 | 23 | 24 | -record(?MODULE, 25 | { unix_time :: float() 26 | }). 27 | 28 | -opaque t() :: 29 | ?T{}. 30 | 31 | 32 | -spec now() -> 33 | t(). 34 | now() -> 35 | Timestamp = os:timestamp(), 36 | of_timestamp(Timestamp). 37 | 38 | -spec of_timestamp(erlang:timestamp()) -> 39 | t(). 40 | of_timestamp({MegasecondsInt, SecondsInt, MicrosecondsInt}) -> 41 | Million = 1000000.0, 42 | Megaseconds = float(MegasecondsInt), 43 | Seconds = float(SecondsInt), 44 | Microseconds = float(MicrosecondsInt), 45 | UnixTime = (Megaseconds * Million) + Seconds + (Microseconds / Million), 46 | ?T{unix_time = UnixTime}. 47 | 48 | -spec to_unix_time(t()) -> 49 | float(). 50 | to_unix_time(?T{unix_time=UnixTime}) -> 51 | UnixTime. 52 | 53 | -spec of_float(float()) -> 54 | t(). 55 | of_float(Float) when is_float(Float) -> 56 | ?T{unix_time = Float}. 57 | 58 | -spec to_float(t()) -> 59 | float(). 60 | to_float(?T{unix_time=Float}) -> 61 | Float. 62 | 63 | -spec of_iso8601(binary()) -> 64 | hope_result:t(t(), {unrecognized_as_iso8601, binary()}). 65 | of_iso8601(<>) -> 66 | % We use regexp rather than just simple binary pattern match, because we 67 | % also want to validate character ranges, i.e., that components are 68 | % integers. 69 | ValidPatterns = 70 | [ {zoneless, <<"\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d">>} 71 | ], 72 | ValidPatternMatchers = 73 | [{Tag, make_regexp_bool(RegExp)} || {Tag, RegExp} <- ValidPatterns], 74 | case hope_list:first_match(ValidPatternMatchers, Bin) 75 | of none -> {error, {unrecognized_as_iso8601, Bin}} 76 | ; {some, zoneless} -> {ok, of_iso8601_zoneless(Bin)} 77 | end. 78 | 79 | -spec of_iso8601_zoneless(binary()) -> 80 | t(). 81 | of_iso8601_zoneless(<>) -> 82 | << YearBin:4/binary, "-", MonthBin:2/binary, "-", DayBin:2/binary 83 | , "T" 84 | , HourBin:2/binary, ":", MinBin:2/binary , ":", SecBin:2/binary 85 | >> = Bin, 86 | Year = binary_to_integer(YearBin), 87 | Month = binary_to_integer(MonthBin), 88 | Day = binary_to_integer(DayBin), 89 | Hour = binary_to_integer(HourBin), 90 | Min = binary_to_integer(MinBin), 91 | Sec = binary_to_integer(SecBin), 92 | DateTime = {{Year, Month, Day}, {Hour, Min, Sec}}, 93 | SecondsGregorian = calendar:datetime_to_gregorian_seconds(DateTime), 94 | SecondsFromZeroToUnixEpoch = 62167219200, 95 | SecondsUnixEpochInt = SecondsGregorian - SecondsFromZeroToUnixEpoch, 96 | SecondsUnixEpoch = float(SecondsUnixEpochInt), 97 | of_float(SecondsUnixEpoch). 98 | 99 | -spec make_regexp_bool(binary()) -> 100 | fun((binary()) -> boolean()). 101 | make_regexp_bool(<>) -> 102 | fun (<>) -> 103 | case re:run(String, RegExp) 104 | of nomatch -> false 105 | ; {match, _} -> true 106 | end 107 | end. 108 | -------------------------------------------------------------------------------- /test/hope_dictionary_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(hope_dictionary_SUITE). 2 | 3 | %% Callbacks 4 | -export( 5 | [ all/0 6 | , groups/0 7 | , init_per_group/2 8 | , end_per_group/2 9 | ]). 10 | 11 | %% Test cases 12 | -export( 13 | [ t_set_new/1 14 | , t_set_existing/1 15 | , t_get/1 16 | , t_pop/1 17 | , t_fold/1 18 | , t_dictionary_specs/1 19 | , t_has_key/1 20 | ]). 21 | 22 | 23 | -define(DICT_MODULE , dict_module). 24 | -define(DICT_MODULE_KV_LIST , hope_kv_list). 25 | 26 | 27 | %% ============================================================================ 28 | %% Common Test callbacks 29 | %% ============================================================================ 30 | 31 | all() -> 32 | [{group, ?DICT_MODULE_KV_LIST}]. 33 | 34 | groups() -> 35 | Tests = 36 | [ t_set_new 37 | , t_set_existing 38 | , t_get 39 | , t_pop 40 | , t_fold 41 | , t_dictionary_specs 42 | , t_has_key 43 | ], 44 | Properties = [parallel], 45 | [{?DICT_MODULE_KV_LIST, Properties, Tests}]. 46 | 47 | init_per_group(DictModule, Cfg) -> 48 | hope_kv_list:set(Cfg, ?DICT_MODULE, DictModule). 49 | 50 | end_per_group(_DictModule, _Cfg) -> 51 | ok. 52 | 53 | 54 | %% ============================================================================= 55 | %% Test cases 56 | %% ============================================================================= 57 | 58 | t_get(Cfg) -> 59 | {some, DictModule} = hope_kv_list:get(Cfg, ?DICT_MODULE), 60 | K1 = k1, 61 | K2 = k2, 62 | V1 = v1, 63 | V2 = v2, 64 | D = DictModule:set(DictModule:empty(), K1, V1), 65 | {some, V1} = DictModule:get(D, K1), 66 | V1 = DictModule:get(D, K1, V2), 67 | none = DictModule:get(D, K2), 68 | V2 = DictModule:get(D, K2, V2), 69 | default = DictModule:get(D, K1, default, fun (X) -> X =:= foo end), 70 | V1 = DictModule:get(D, K1, default, fun (X) -> X =:= V1 end). 71 | 72 | t_set_new(Cfg) -> 73 | {some, DictModule} = hope_kv_list:get(Cfg, ?DICT_MODULE), 74 | Key = key, 75 | ValExpected = bar, 76 | ListInitial = DictModule:empty(), 77 | ListResulting = DictModule:set(ListInitial, Key, ValExpected), 78 | {some, ValResulting} = DictModule:get(ListResulting, Key), 79 | ValResulting = ValExpected. 80 | 81 | t_set_existing(Cfg) -> 82 | {some, DictModule} = hope_kv_list:get(Cfg, ?DICT_MODULE), 83 | Key = key, 84 | ValInitial = foo, 85 | ValExpected = bar, 86 | ListInitial = [{donald, duck}, {Key, ValInitial}], 87 | ListResulting = DictModule:set(ListInitial, Key, ValExpected), 88 | {some, ValResulting} = DictModule:get(ListResulting, Key), 89 | ValResulting = ValExpected. 90 | 91 | t_pop(Cfg) -> 92 | {some, DictModule} = hope_kv_list:get(Cfg, ?DICT_MODULE), 93 | KVList = [{a, 1}, {b, 2}, {c, 3}], 94 | Dict1 = DictModule:of_kv_list(KVList), 95 | {{some, 1} , Dict2} = DictModule:pop(Dict1, a), 96 | {none , Dict3} = DictModule:pop(Dict2, a), 97 | {{some, 2} , Dict4} = DictModule:pop(Dict3, b), 98 | {none , Dict5} = DictModule:pop(Dict4, b), 99 | {{some, 3} , Dict6} = DictModule:pop(Dict5, c), 100 | {none , Dict7} = DictModule:pop(Dict6, c), 101 | [] = DictModule:to_kv_list(Dict7). 102 | 103 | t_fold(Cfg) -> 104 | {some, DictModule} = hope_kv_list:get(Cfg, ?DICT_MODULE), 105 | KVList = [{a, 1}, {a, 5}, {b, 3}, {c, 4}, {c, 4}], 106 | Dict = DictModule:of_kv_list(KVList), 107 | 17 = DictModule:fold(Dict, fun (_K, V, Acc) -> V + Acc end, 0). 108 | 109 | t_dictionary_specs(Cfg) -> 110 | {some, DictModule} = hope_kv_list:get(Cfg, ?DICT_MODULE), 111 | [] = proper:check_specs(DictModule). 112 | 113 | t_has_key(Cfg) -> 114 | {some, DictModule} = hope_kv_list:get(Cfg, ?DICT_MODULE), 115 | D = DictModule:of_kv_list([{a, 1}, {b, 2}, {c, 3}]), 116 | true = DictModule:has_key(D, a), 117 | true = DictModule:has_key(D, b), 118 | true = DictModule:has_key(D, c), 119 | false = DictModule:has_key(D, d), 120 | false = DictModule:has_key(D, e), 121 | false = DictModule:has_key(D, f). 122 | -------------------------------------------------------------------------------- /test/hope_fun_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(hope_fun_SUITE). 2 | 3 | %% Callbacks 4 | -export( 5 | [ all/0 6 | , groups/0 7 | ]). 8 | 9 | %% Test cases 10 | -export( 11 | [ t_specs/1 12 | , t_id/1 13 | , t_curry/1 14 | , t_compose_and_thread/1 15 | ]). 16 | 17 | 18 | -define(GROUP, hope_fun). 19 | 20 | 21 | %% ============================================================================ 22 | %% Common Test callbacks 23 | %% ============================================================================ 24 | 25 | all() -> 26 | [ {group, ?GROUP} 27 | ]. 28 | 29 | groups() -> 30 | Tests = 31 | [ t_specs 32 | , t_id 33 | , t_curry 34 | , t_compose_and_thread 35 | ], 36 | Properties = [parallel], 37 | [ {?GROUP, Properties, Tests} 38 | ]. 39 | 40 | 41 | %% ============================================================================= 42 | %% Test cases 43 | %% ============================================================================= 44 | 45 | t_specs(_) -> 46 | [] = proper:check_specs(hope_fun). 47 | 48 | t_id(_Cfg) -> 49 | X = foo, 50 | X = hope_fun:id(X). 51 | 52 | t_curry(_Cfg) -> 53 | Single = fun (X) -> X end, 54 | Double = fun (X, Y) -> {X, Y} end, 55 | Triple = fun (X, Y, Z) -> {X, Y, Z} end, 56 | 57 | F = hope_fun:curry(Single), 58 | a = F(a), 59 | 60 | G1 = hope_fun:curry(Double), 61 | G = G1(a), 62 | {a, b} = G(b), 63 | 64 | H1 = hope_fun:curry(Triple), 65 | H2 = H1(a), 66 | H = H2(b), 67 | {a, b, c} = H(c). 68 | 69 | t_compose_and_thread(_Cfg) -> 70 | A2B = fun (a) -> b end, 71 | B2C = fun (b) -> c end, 72 | C2D = fun (c) -> d end, 73 | Fs = [C2D, B2C, A2B], 74 | d = (hope_fun:compose ( Fs ))(a), 75 | d = (hope_fun:compose_right ( Fs ))(a), 76 | d = (hope_fun:compose_left (lists:reverse(Fs) ))(a), 77 | d = hope_fun:thread (lists:reverse(Fs), a). 78 | -------------------------------------------------------------------------------- /test/hope_kv_list_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(hope_kv_list_SUITE). 2 | 3 | -include_lib("hope_kv_list.hrl"). 4 | 5 | %% Callbacks 6 | -export( 7 | [ all/0 8 | , groups/0 9 | ]). 10 | 11 | %% Test cases 12 | -export( 13 | [ t_validate_unique_presence/1 14 | ]). 15 | 16 | 17 | -define(GROUP , hope_kv_list). 18 | 19 | 20 | %% ============================================================================ 21 | %% Common Test callbacks 22 | %% ============================================================================ 23 | 24 | all() -> 25 | [{group, ?GROUP}]. 26 | 27 | groups() -> 28 | Tests = 29 | [ t_validate_unique_presence 30 | ], 31 | Properties = [], 32 | [{?GROUP, Properties, Tests}]. 33 | 34 | 35 | %% ============================================================================= 36 | %% Test cases 37 | %% ============================================================================= 38 | 39 | t_validate_unique_presence(_Cfg) -> 40 | KeysRequired = [a, b, c], 41 | DictOk = [{a, 1}, {b, 2}, {c, 3}], 42 | DictUnsup = [{a, 1}, {b, 2}, {c, 3}, {d, 4}], 43 | DictDups = [{a, 1}, {b, 2}, {c, 3}, {a, 4}], 44 | DictMissing = [{a, 1}, {b, 2}], 45 | 46 | {ok, DictOk} = 47 | hope_kv_list:validate_unique_presence(DictOk, KeysRequired), 48 | #hope_kv_list_presence_violations 49 | { keys_missing = [] 50 | , keys_duplicated = [] 51 | , keys_unsupported = [] 52 | } = 53 | hope_kv_list:find_unique_presence_violations(DictOk, KeysRequired), 54 | 55 | {error, [{keys_unsupported, [d]}]} = 56 | hope_kv_list:validate_unique_presence(DictUnsup, KeysRequired), 57 | #hope_kv_list_presence_violations 58 | { keys_missing = [] 59 | , keys_duplicated = [] 60 | , keys_unsupported = [d] 61 | } = 62 | hope_kv_list:find_unique_presence_violations(DictUnsup, KeysRequired), 63 | 64 | {error, [{keys_duplicated, [a]}]} = 65 | hope_kv_list:validate_unique_presence(DictDups, KeysRequired), 66 | #hope_kv_list_presence_violations 67 | { keys_missing = [] 68 | , keys_duplicated = [a] 69 | , keys_unsupported = [] 70 | } = 71 | hope_kv_list:find_unique_presence_violations(DictDups, KeysRequired), 72 | 73 | {error, [{keys_missing, [c]}]} = 74 | hope_kv_list:validate_unique_presence(DictMissing, KeysRequired), 75 | #hope_kv_list_presence_violations 76 | { keys_missing = [c] 77 | , keys_duplicated = [] 78 | , keys_unsupported = [] 79 | } = 80 | hope_kv_list:find_unique_presence_violations(DictMissing, KeysRequired). 81 | -------------------------------------------------------------------------------- /test/hope_list_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(hope_list_SUITE). 2 | 3 | -include_lib("proper/include/proper_common.hrl"). 4 | 5 | %% Callbacks 6 | -export( 7 | [ all/0 8 | , groups/0 9 | ]). 10 | 11 | %% Test cases 12 | -export( 13 | [ t_auto_hope_list_specs/1 14 | , t_auto_map/1 15 | , t_auto_map_3/1 16 | , t_auto_map_rev/1 17 | , t_auto_map_slow/1 18 | , t_auto_unique_preserve_order/1 19 | , t_manual_map/1 20 | , t_manual_map_result/1 21 | , t_manual_map_rev/1 22 | , t_manual_map_slow/1 23 | , t_manual_divide/1 24 | ]). 25 | 26 | 27 | -define(GROUP , hope_list). 28 | 29 | -define(TEST(TestSpec), true = proper:quickcheck(TestSpec)). 30 | 31 | -define(type, proper_types). 32 | 33 | 34 | %% ============================================================================ 35 | %% Common Test callbacks 36 | %% ============================================================================ 37 | 38 | all() -> 39 | [{group, ?GROUP}]. 40 | 41 | groups() -> 42 | Tests = 43 | [ t_auto_hope_list_specs 44 | , t_auto_map 45 | , t_auto_map_3 46 | , t_auto_map_rev 47 | , t_auto_map_slow 48 | , t_auto_unique_preserve_order 49 | , t_manual_map 50 | , t_manual_map_result 51 | , t_manual_map_rev 52 | , t_manual_map_slow 53 | , t_manual_divide 54 | ], 55 | Properties = [parallel], 56 | [{?GROUP, Properties, Tests}]. 57 | 58 | %% ============================================================================= 59 | %% Manual test cases 60 | %% ============================================================================= 61 | 62 | t_manual_map(_Cfg) -> 63 | F = fun (N) -> N + 1 end, 64 | Xs = lists:seq(1, 5010), 65 | Ys = lists:map(F, Xs), 66 | Ys = hope_list:map(Xs, F), 67 | [] = hope_list:map([], F). 68 | 69 | t_manual_map_result(_Cfg) -> 70 | AssertPositive = 71 | fun (I) when I > 0 -> {ok, I}; (_) -> {error, negative} end, 72 | AllPositives = lists:seq(1, 5), 73 | AllNegatives = lists:seq(-5, -1), 74 | Mixed = lists:seq(-5, 5), 75 | {ok, AllPositives} = hope_list:map_result(AllPositives, AssertPositive), 76 | {error, negative} = hope_list:map_result(AllNegatives, AssertPositive), 77 | {error, negative} = hope_list:map_result(Mixed, AssertPositive). 78 | 79 | t_manual_map_rev(_Cfg) -> 80 | F = fun (N) -> N + 1 end, 81 | [4, 3, 2] = hope_list:map_rev([1, 2, 3], F), 82 | [] = hope_list:map_rev([], F). 83 | 84 | t_manual_map_slow(_Cfg) -> 85 | F = fun (N) -> N + 1 end, 86 | [2, 3, 4] = hope_list:map_slow([1, 2, 3], F), 87 | [] = hope_list:map_slow([], F). 88 | 89 | t_manual_divide(_Cfg) -> 90 | try 91 | hope_list:divide([a, b, c], -1) 92 | catch 93 | error:hope_list__divide__size_must_be_a_positive_integer -> ok 94 | end, 95 | try 96 | hope_list:divide([a, b, c], 0) 97 | catch 98 | error:hope_list__divide__size_must_be_a_positive_integer -> ok 99 | end, 100 | [[c], [b], [a]] = hope_list:divide([a, b, c], 1), 101 | [[c], [b, a]] = hope_list:divide([a, b, c], 2), 102 | [[c, b, a]] = hope_list:divide([a, b, c], 3), 103 | [[c, b, a]] = hope_list:divide([a, b, c], 4), 104 | [[c, b, a]] = hope_list:divide([a, b, c], 5), 105 | try 106 | hope_list:divide([], 0) 107 | catch 108 | error:hope_list__divide__size_must_be_a_positive_integer -> ok 109 | end, 110 | try 111 | hope_list:divide([], -1) 112 | catch 113 | error:hope_list__divide__size_must_be_a_positive_integer -> ok 114 | end, 115 | [[f, e], [d, c], [b, a]] = hope_list:divide([a, b, c, d, e, f], 2), 116 | [[f, e, d], [c, b, a]] = hope_list:divide([a, b, c, d, e, f], 3). 117 | 118 | %% ============================================================================= 119 | %% Generated test cases 120 | %% ============================================================================= 121 | 122 | t_auto_map_rev(_Cfg) -> 123 | ?TEST(?FORALL({L, F}, {type_l(), type_f()}, 124 | hope_list:map_rev(L, F) == lists:reverse(lists:map(F, L)) 125 | )). 126 | 127 | t_auto_map_slow(_Cfg) -> 128 | ?TEST(?FORALL({L, F}, {type_l(), type_f()}, 129 | hope_list:map_slow(L, F) == lists:map(F, L) 130 | )). 131 | 132 | t_auto_map(_Cfg) -> 133 | ?TEST(?FORALL({L, F}, {type_l(), type_f()}, 134 | hope_list:map(L, F) == lists:map(F, L) 135 | )). 136 | 137 | t_auto_map_3(_Cfg) -> 138 | ?TEST(?FORALL({L, F, N}, {type_l(), type_f(), ?type:non_neg_integer()}, 139 | hope_list:map(L, F, N) == lists:map(F, L) 140 | )). 141 | 142 | t_auto_unique_preserve_order(_Cfg) -> 143 | ?TEST(?FORALL(L, ?type:list(), 144 | begin 145 | Duplicates = L -- lists:usort(L), 146 | UniquesInOrderA = lists:reverse(lists:reverse(L) -- Duplicates), 147 | UniquesInOrderB = hope_list:unique_preserve_order(L), 148 | UniquesInOrderA == UniquesInOrderB 149 | end)). 150 | 151 | t_auto_hope_list_specs(_Cfg) -> 152 | [] = proper:check_specs(hope_list). 153 | 154 | %% ============================================================================ 155 | %% Common types 156 | %% ============================================================================ 157 | 158 | type_l() -> 159 | ?type:list(?type:integer()). 160 | 161 | type_f() -> 162 | ?type:function([?type:integer()], ?type:term()). 163 | -------------------------------------------------------------------------------- /test/hope_option_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(hope_option_SUITE). 2 | 3 | %% Callbacks 4 | -export( 5 | [ all/0 6 | , groups/0 7 | ]). 8 | 9 | %% Test cases 10 | -export( 11 | [ t_of_result/1 12 | , t_undefined/1 13 | , t_put/1 14 | , t_get/1 15 | , t_map/1 16 | , t_iter/1 17 | , t_pipe/1 18 | , t_validate/1 19 | ]). 20 | 21 | 22 | -define(GROUP, option). 23 | 24 | 25 | %% ============================================================================ 26 | %% Common Test callbacks 27 | %% ============================================================================ 28 | 29 | all() -> 30 | [ {group, ?GROUP} 31 | ]. 32 | 33 | groups() -> 34 | Tests = 35 | [ t_of_result 36 | , t_undefined 37 | , t_put 38 | , t_get 39 | , t_map 40 | , t_iter 41 | , t_pipe 42 | , t_validate 43 | ], 44 | Properties = [parallel], 45 | [ {?GROUP, Properties, Tests} 46 | ]. 47 | 48 | 49 | %% ============================================================================= 50 | %% Test cases 51 | %% ============================================================================= 52 | 53 | t_put(_Cfg) -> 54 | IsFoo = fun (foo) -> true; (_) -> false end, 55 | {some, foo} = hope_option:put(foo, IsFoo), 56 | none = hope_option:put(bar, IsFoo). 57 | 58 | t_get(_Cfg) -> 59 | foo = hope_option:get({some, foo}, bar), 60 | bar = hope_option:get(none , bar). 61 | 62 | t_map(_Cfg) -> 63 | FooToBar = fun (foo) -> bar end, 64 | {some, bar} = hope_option:map({some, foo}, FooToBar), 65 | none = hope_option:map(none , FooToBar). 66 | 67 | t_iter(_Cfg) -> 68 | Key = key, 69 | Put = fun (Val) -> put(Key, Val) end, 70 | Get = fun () -> get(Key) end, 71 | Val = foo, 72 | {} = hope_option:iter(none , Put), 73 | undefined = Get(), 74 | {} = hope_option:iter({some, Val}, Put), 75 | Val = Get(). 76 | 77 | t_of_result(_Cfg) -> 78 | Foo = foo, 79 | Bar = bar, 80 | ResultOk = {ok, Foo}, 81 | ResultError = {error, Bar}, 82 | {some, Foo} = hope_option:of_result(ResultOk), 83 | none = hope_option:of_result(ResultError). 84 | 85 | t_pipe(_Cfg) -> 86 | Steps = 87 | [ fun (0) -> hope_option:return(1); (_) -> none end 88 | , fun (1) -> hope_option:return(2); (_) -> none end 89 | , fun (2) -> hope_option:return(3); (_) -> none end 90 | ], 91 | {some, 3} = hope_option:pipe(Steps, 0), 92 | none = hope_option:pipe(Steps, 1), 93 | none = hope_option:pipe(Steps, 2), 94 | none = hope_option:pipe(Steps, 3). 95 | 96 | t_undefined(_Cfg) -> 97 | X = foo, 98 | {some, X} = hope_option:of_undefined(X), 99 | X = hope_option:to_undefined({some, X}), 100 | none = hope_option:of_undefined(undefined), 101 | undefined = hope_option:to_undefined(none). 102 | 103 | t_validate(_Cfg) -> 104 | IsFoo = fun (X) -> X =:= foo end, 105 | none = hope_option:validate(none, IsFoo), 106 | none = hope_option:validate({some, bar}, IsFoo), 107 | {some, foo} = hope_option:validate({some, foo}, IsFoo). 108 | -------------------------------------------------------------------------------- /test/hope_result_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(hope_result_SUITE). 2 | 3 | %% Callbacks 4 | -export( 5 | [ all/0 6 | , groups/0 7 | , init_per_group/2 8 | , end_per_group/2 9 | ]). 10 | 11 | %% Test cases 12 | -export( 13 | [ t_pipe_ok/1 14 | , t_pipe_error/1 15 | , t_hope_result_specs/1 16 | , t_lift_exn/1 17 | , t_lift_map_exn/1 18 | , t_return/1 19 | , t_map/1 20 | , t_map_error/1 21 | , t_tag_error/1 22 | ]). 23 | 24 | 25 | -define(GROUP_PIPE, result_pipe). 26 | -define(GROUP_SPEC, result_spec). 27 | -define(GROUP_LIFT, result_lift_exn). 28 | -define(GROUP_OTHER, result_other). 29 | 30 | 31 | %% ============================================================================ 32 | %% Common Test callbacks 33 | %% ============================================================================ 34 | 35 | all() -> 36 | [ {group, ?GROUP_PIPE} 37 | , {group, ?GROUP_SPEC} 38 | , {group, ?GROUP_LIFT} 39 | , {group, ?GROUP_OTHER} 40 | ]. 41 | 42 | groups() -> 43 | PipeTests = 44 | [ t_pipe_ok 45 | , t_pipe_error 46 | ], 47 | SpecTests = 48 | [ t_hope_result_specs 49 | ], 50 | LiftTests = 51 | [ t_lift_exn 52 | , t_lift_map_exn 53 | ], 54 | OtherTests = 55 | [ t_return 56 | , t_map 57 | , t_map_error 58 | , t_tag_error 59 | ], 60 | Properties = [parallel], 61 | [ {?GROUP_PIPE, Properties, PipeTests} 62 | , {?GROUP_SPEC, Properties, SpecTests} 63 | , {?GROUP_LIFT, Properties, LiftTests} 64 | , {?GROUP_OTHER, Properties, OtherTests} 65 | ]. 66 | 67 | init_per_group(?GROUP_PIPE, Cfg) -> 68 | Steps = 69 | [ fun (0) -> {ok, 1}; (X) -> {error, X} end 70 | , fun (1) -> {ok, 2}; (X) -> {error, X} end 71 | , fun (2) -> {ok, 3}; (X) -> {error, X} end 72 | ], 73 | hope_kv_list:set(Cfg, steps, Steps); 74 | init_per_group(_, Cfg) -> 75 | Cfg. 76 | 77 | end_per_group(_, _Cfg) -> 78 | ok. 79 | 80 | 81 | %% ============================================================================= 82 | %% Test cases 83 | %% ============================================================================= 84 | 85 | t_pipe_ok(Cfg) -> 86 | {some, Steps} = hope_kv_list:get(Cfg, steps), 87 | {ok, 3} = hope_result:pipe(Steps, 0). 88 | 89 | t_pipe_error(Cfg) -> 90 | {some, Steps} = hope_kv_list:get(Cfg, steps), 91 | {error, 1} = hope_result:pipe(Steps, 1). 92 | 93 | t_hope_result_specs(_) -> 94 | [] = proper:check_specs(hope_result). 95 | 96 | t_lift_exn(_Cfg) -> 97 | Class = throw, 98 | Reason = foofoo, 99 | Label = bar, 100 | F = fun (ok) -> apply(erlang, Class, [Reason]) end, 101 | G = hope_result:lift_exn(F), 102 | H = hope_result:lift_exn(F, Label), 103 | {error, {Class, Reason}} = G(ok), 104 | {error, {Label, {Class, Reason}}} = H(ok). 105 | 106 | t_lift_map_exn(_Cfg) -> 107 | FOk = fun ({}) -> foo end, 108 | FExn = fun ({}) -> throw(baz) end, 109 | MapOk = fun (foo) -> bar end, 110 | MapOkThrows = fun (foo) -> throw(exn_from_ok_map) end, 111 | MapError = fun ({throw, baz}) -> qux end, 112 | MapErrorThrows = fun ({throw, baz}) -> throw(exn_from_error_map) end, 113 | GOk = hope_result:lift_map_exn(FOk , MapOk , MapError), 114 | GOkThrows = hope_result:lift_map_exn(FOk , MapOkThrows, MapError), 115 | GError = hope_result:lift_map_exn(FExn, MapOk , MapError), 116 | GErrorThrows = hope_result:lift_map_exn(FExn, MapOk , MapErrorThrows), 117 | {ok, bar} = GOk({}), 118 | {error, qux} = GError({}), 119 | ok = 120 | try 121 | must_not_return = GOkThrows({}) 122 | catch throw:exn_from_ok_map -> 123 | ok 124 | end, 125 | ok = 126 | try 127 | must_not_return = GErrorThrows({}) 128 | catch throw:exn_from_error_map -> 129 | ok 130 | end. 131 | 132 | t_return(_Cfg) -> 133 | X = foo, 134 | {ok, X} = hope_result:return(X). 135 | 136 | t_map(_Cfg) -> 137 | X = foo, 138 | Y = bar, 139 | F = fun (foo) -> Y end, 140 | {ok, Y} = hope_result:map({ok, X}, F), 141 | {error, X} = hope_result:map({error, X}, F). 142 | 143 | t_map_error(_Cfg) -> 144 | X = foo, 145 | Y = bar, 146 | XtoY = fun (foo) -> Y end, 147 | {ok , X} = hope_result:map_error({ok , X}, XtoY), 148 | {error, Y} = hope_result:map_error({error, X}, XtoY). 149 | 150 | t_tag_error(_Cfg) -> 151 | X = foo, 152 | Tag = bar, 153 | {ok , X } = hope_result:tag_error({ok , X}, Tag), 154 | {error, {Tag, X}} = hope_result:tag_error({error, X}, Tag). 155 | --------------------------------------------------------------------------------