-module(bt_blackboard). -export([ get_empty/0, get_bb_key/2, is_set/2, %% not exporting read since eval is enough. otherwise its easy to make mistakes % read/3, write/3, promise/2, clear_namespace/2, eval/2, eval_with_default_fn/3, throw_error_fn/1 ]). -record(bb_key, { namespace :: list(string()), key :: string() }). -record(bb_promise, { args :: list(term()), promise_fn :: function() }). %% since implemented as map get_empty() -> #{}. get_bb_key(Namespace, Key) -> #bb_key{namespace = Namespace, key = Key}. is_set(BBKey, Blackboard) when is_record(BBKey, bb_key) -> case maps:get(BBKey, Blackboard) of null -> false; {value, _} -> true end; %% is not a key, hence set is_set(_NotKey, _Blackboard) -> true. read(BBKey, Blackboard, OnNull) when is_record(BBKey, bb_key), is_function(OnNull) -> case maps:get(BBKey, Blackboard) of null -> OnNull(BBKey); {value, Val} -> Val; ErrorVal -> ErrorReason = lists:flatten(io_lib:format("Incorrect value stored for key '~p' as '~p'~n", [BBKey, ErrorVal])), erlang:error(ErrorReason) end; %% is not a key, so return it. read(NotKey, _Blackboard, _OnNull) -> NotKey. write(BBKey, Value, Blackboard) when is_record(BBKey, bb_key)-> maps:put(BBKey, {value, Value}, Blackboard); write(NotKey, _Value, _Blackboard) -> ErrorReason = lists:flatten(io_lib:format("Trying to write to ~p which is not a bb key~n", [NotKey])), erlang:error(ErrorReason). %% make a promise that will be evaluated at runtime! %% eg. from converting between coordinate and barcode, this can be used %% to skip a conversion subtask. promise(Args, PromiseFn) -> #bb_promise{ args = Args, promise_fn = PromiseFn }. %% set all keys within this namespace to null %% sometimes namespace is undefined for a sequence/selector, in %% that case do nothing. clear_namespace(undefined, Blackboard) -> Blackboard; clear_namespace(Namespace, Blackboard) -> BBAsProplist = maps:to_list(Blackboard), %% find stuff that is prefix of namespace and set value to null NewBBProplist = lists:map( fun({BBKey, BBVal}) -> BBNS = BBKey#bb_key.namespace, case lists:prefix(Namespace, BBNS) of true -> {BBKey, null}; false -> {BBKey, BBVal} end end, BBAsProplist ), maps:from_list(NewBBProplist). %% evaluation function! %% input can be key, array of keys, tuple of keys, non-key eval(BBKey, Blackboard)-> eval_internal(BBKey, Blackboard, fun bt_blackboard:throw_error_fn/1 ). %% with function that returns default value in case bb key value is null eval_with_default_fn(BBKey, Blackboard, OnNull)-> eval_internal(BBKey, Blackboard, OnNull). %% evaluate promise eval_internal(BBPromise = #bb_promise{args = Args, promise_fn = PromiseFn}, Blackboard, OnNull) when is_record(BBPromise, bb_promise) -> EvaledArgs = eval_internal(Args, Blackboard, OnNull), erlang:apply(PromiseFn, EvaledArgs); eval_internal(BBKey, Blackboard, OnNull) when is_record(BBKey, bb_key) -> read(BBKey, Blackboard, OnNull); eval_internal(ListOfVals, Blackboard, OnNull) when is_list(ListOfVals) -> [eval_internal(X, Blackboard, OnNull) || X <- ListOfVals]; eval_internal(TupleOfVals, Blackboard, OnNull) when is_tuple(TupleOfVals) -> L = tuple_to_list(TupleOfVals), %% need to convert back to tuple too... L2 = eval_internal(L, Blackboard, OnNull), list_to_tuple(L2); eval_internal(Native, _Blackboard, _OnNull) -> Native. throw_error_fn(BBKey) -> ErrorReason = lists:flatten(io_lib:format("Trying to read uninitalized key '~p'~n", [BBKey])), erlang:error(ErrorReason).