From 5135a55081f93eff46b53ccf0bbacad8f225516e Mon Sep 17 00:00:00 2001 From: Peter Harpending Date: Mon, 29 Sep 2025 13:34:08 -0700 Subject: [PATCH] wip --- 2 | 110 +++++++++++++++++++++++++++++++++++++++++++ src/wfc.erl | 22 +++++++++ src/wfc_ltr.erl | 14 ++++-- src/wfc_sentence.erl | 75 +++++++++++++++++++++++++++++ src/wfc_utils.erl | 4 +- src/wfc_word.erl | 56 ++++++++++++++++++---- 6 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 2 create mode 100644 src/wfc.erl create mode 100644 src/wfc_sentence.erl diff --git a/2 b/2 new file mode 100644 index 0000000..b7b3e7c --- /dev/null +++ b/2 @@ -0,0 +1,110 @@ +% @doc a word is an ordset of ltrs +% +% This mathematically is a cluster of letters; so in "A + B + AB", this is one +% of the summands. +% +% multiplication is implied in a word +% +% empty word is 1 +% +% anything times itself equals itself, so duplication is ignored +% +% multiplication = inclusive union +% +% operations assume all inputs are valid +-module(wfc_word). + +-export_type([ + word/0 +]). + +-export([ + %% constructors + one/0, + validate/1, + from_list/1, to_list/1, + %% ops + mul/2, mul/1 +]). + +-opaque word() :: {w, ordsets:ordset(wfc_ltr:ltr())}. + +%%---------------------------- +%% constructor +%%---------------------------- + +-spec one() -> word(). + +one() -> + {w, []}. + + +-spec validate(Word) -> Result + when Word :: word(), + Result :: ok + | {error, Reason :: string()}. +% @doc +% check each letter in the word for validity +% +% also check that word shape is valid +% @end + +validate(W = {w, Letters}) -> + case from_list(Letters) of + {ok, Result} when Result =:= W -> + ok; + Error -> + Error + end; +validate(X) -> + {error, wfc_utils:str("wfc_word:validate: malformed word: ~tp", [X])}. + + + +-spec to_list(word()) -> Result + when Result :: {ok, [wfc_ltr:ltr()]} + | {error, string()}. + +to_list({w, Ltrs}) -> {ok, Ltrs}; +to_list(Bad) -> {error, wfc_utils:str("wfc_word:to_list: bad letter: ~tp", [Bad])}. + + + +-spec from_list(Ltrs) -> Result + when Ltrs :: list(wfc_ltr:ltr()), + Result :: {ok, word()} + | {error, Reason :: string()}. + +from_list(Chars) -> + from_list(ordsets:from_list(Chars), []). + + +%% validate each letter +from_list([Ltr | Rest], Acc) -> + case wfc_ltr:validate(Ltr) of + ok -> from_list(Rest, [Ltr | Acc]); + Error -> Error + end; +% done, all good +from_list([], Acc) -> + {ok, {w, lists:reverse(Acc)}}. + + +%%---------------------------- +%% ops +%%---------------------------- + +-spec mul(word(), word()) -> word(). +% @doc product of two words +% +% assumes the words are valid + +mul({w, X}, {w, Y}) -> + {w, ordsets:union(X, Y)}. + + +-spec mul([word()]) -> word(). +% @doc multiply a list of words together + +mul([Word | Rest]) -> mul(Word, mul(Rest)); +mul([]) -> one(). diff --git a/src/wfc.erl b/src/wfc.erl new file mode 100644 index 0000000..8512811 --- /dev/null +++ b/src/wfc.erl @@ -0,0 +1,22 @@ +% @doc +% porcelain wfc ops +-module(wfc). + +-export_type([ + sentence/0 +]). + +-export([ + + zero/0, one/0, + add/1, add/2, + mul/1, mul/2 +]). + + +-type sentence() :: wfc_sentence:sentence(). + + +%% constructors +-spec +zero() diff --git a/src/wfc_ltr.erl b/src/wfc_ltr.erl index 5ce8772..d198080 100644 --- a/src/wfc_ltr.erl +++ b/src/wfc_ltr.erl @@ -1,4 +1,9 @@ -% @doc called ltr to disambiguate from +% @doc called ltr to disambiguate from "char" +% +% In "A + B + AB", this is one of the individual letters. +% +% a letter is a binary. atom is more natural representation but this is http, +% can't be spamming the atoms table. % % mathematically, this is a variable like "a", "b", "c", etc -module(wfc_ltr). @@ -78,6 +83,9 @@ new2(Acc, Rest = <>) -> --spec to_binary(ltr()) -> binary(). +-spec to_binary(ltr()) -> Result + when Result :: {ok, binary()} + | {error, string()}. -to_binary({c, X}) -> X. +to_binary({c, X}) -> {ok, X}; +to_binary(Bad) -> {error, wfc_utils:str("wfc_ltr:to_binary: bad letter: ~tp", [Bad])}. diff --git a/src/wfc_sentence.erl b/src/wfc_sentence.erl new file mode 100644 index 0000000..06d019e --- /dev/null +++ b/src/wfc_sentence.erl @@ -0,0 +1,75 @@ +% @doc a sentence is an ordset of words +% +% in "A + B + AB", this is the full expression +% +% summation is implied in a sentence +% +% empty sentence is 0 +-module(wfc_sentence). + +-export_type([ + sentence/0 +]). + +-export([ + %% constructors + zero/0, + validate/1, + from_list/1, to_list/1, + %% ops + add/2, add/1 +]). + +-opaque sentence() :: {s, ordsets:ordset(wfc_word:word())}. + +%%-------------------------- +%% constructors +%%-------------------------- + +-spec zero() :: sentence(). + +zero() -> + {s, []}. + + +-spec from_list(Words) -> Result + when Words :: [wfc_word:word()], + Result :: {ok, sentence()} + | {error, string()}. + +from_list(Words) -> + from_list(ordsets:from_list(Words), []). + +%% validate each word +from_list([W | Rest], Acc) -> + case wfc_word:validate(W) of + ok -> from_list(Rest, [W | Acc]); + Error -> Error + end; +% done, all good +from_list([], Acc) -> + {ok, {s, lists:reverse(Acc)}}. + + + +-spec to_list(sentence()) -> Result + when Result :: {ok, [wfc_word:word()]} + | {error, string()}. + +to_list({s, Words}) -> {ok, Words}; +to_list(Bad) -> {error, wfc_utils:str("wfc_sentence:to_list: bad sentence: ~tp", [Bad])}. + + +%%--------------------------- +%% prim ops +%%--------------------------- + +add({s, X}, {s, Y}) -> {s, disjoint_union(X, Y)}. + +disjoint_union(X, Y) -> + ordsets:subtract(ordsets:union(X, Y), + ordsets:intersection(X, Y)). + + +add([S | Rest]) -> add(S, add(Rest)); +add([]) -> zero(). diff --git a/src/wfc_utils.erl b/src/wfc_utils.erl index 454494e..e2f1dc5 100644 --- a/src/wfc_utils.erl +++ b/src/wfc_utils.erl @@ -1,10 +1,12 @@ % @doc misc utility functions -module(wfc_utils). --export([err/2, emsg/2]). +-export([err/2, str/2, emsg/2]). err(Fmt, Pat) -> {error, emsg(Fmt, Pat)}. +str(X, Y) -> emsg(X, Y). + emsg(Fmt, Pat) -> unicode:characters_to_list(io_lib:format(Fmt, Pat)). diff --git a/src/wfc_word.erl b/src/wfc_word.erl index 2a74ad0..04888ae 100644 --- a/src/wfc_word.erl +++ b/src/wfc_word.erl @@ -1,10 +1,17 @@ % @doc a word is an ordset of ltrs % +% This mathematically is a cluster of letters; so in "A + B + AB", this is one +% of the summands. +% % multiplication is implied in a word % % empty word is 1 % % anything times itself equals itself, so duplication is ignored +% +% multiplication = inclusive union +% +% operations assume all inputs are valid -module(wfc_word). -export_type([ @@ -15,7 +22,7 @@ %% constructors one/0, validate/1, - from_ltrs/1, to_ltrs/1, + from_list/1, to_list/1, %% ops mul/2, mul/1 ]). @@ -32,26 +39,54 @@ one() -> {w, []}. -validate(_) -> error(nyi). -to_ltrs(_) -> error(nyi). +-spec validate(Word) -> Result + when Word :: word(), + Result :: ok + | {error, Reason :: string()}. +% @doc +% check each letter in the word for validity +% +% also check that word shape is valid +% @end --spec from_ltrs(Ltrs) -> Result +validate(W = {w, Letters}) -> + case from_list(Letters) of + {ok, Result} when Result =:= W -> + ok; + Error -> + Error + end; +validate(X) -> + {error, wfc_utils:str("wfc_word:validate: malformed word: ~tp", [X])}. + + + +-spec to_list(word()) -> Result + when Result :: {ok, [wfc_ltr:ltr()]} + | {error, string()}. + +to_list({w, Ltrs}) -> {ok, Ltrs}; +to_list(Bad) -> {error, wfc_utils:str("wfc_word:to_list: bad letter: ~tp", [Bad])}. + + + +-spec from_list(Ltrs) -> Result when Ltrs :: list(wfc_ltr:ltr()), Result :: {ok, word()} | {error, Reason :: string()}. -from_ltrs(Chars) -> - from_ltrs(ordsets:from_list(Chars), []). +from_list(Chars) -> + from_list(ordsets:from_list(Chars), []). %% validate each letter -from_ltrs([Ltr | Rest], Acc) -> +from_list([Ltr | Rest], Acc) -> case wfc_ltr:validate(Ltr) of - ok -> from_ltrs(Rest, [Ltr | Acc]); + ok -> from_list(Rest, [Ltr | Acc]); Error -> Error end; % done, all good -from_ltrs([], Acc) -> +from_list([], Acc) -> {ok, {w, lists:reverse(Acc)}}. @@ -73,3 +108,6 @@ mul({w, X}, {w, Y}) -> mul([Word | Rest]) -> mul(Word, mul(Rest)); mul([]) -> one(). + + +add(