├── src_hs ├── .gitignore ├── Makefile ├── Main5.hs ├── Main4.hs └── BookShop.hs ├── .gitignore ├── docs ├── Presentation.pptx ├── todo.org └── notes.org ├── rebar.lock ├── TODO.md ├── mix.lock ├── src_erl ├── books_shop.app.src ├── main_6.erl ├── main_3.erl ├── wannabe_haskell.erl ├── main_5.erl ├── main_4.erl ├── main_1.erl ├── pipeline.erl ├── main_2.erl └── books_shop.erl ├── rebar.config ├── mix.exs ├── src_ex ├── Main5.ex ├── Main6.ex ├── Main3.ex ├── Pipeline.ex ├── Main1.ex ├── Main4.ex ├── Main2.ex └── BookShop.ex └── README.md /src_hs/.gitignore: -------------------------------------------------------------------------------- 1 | book_shop* 2 | out -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.beam 4 | _build 5 | deps -------------------------------------------------------------------------------- /docs/Presentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzh44yzh/erl_fun_composition/HEAD/docs/Presentation.pptx -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | [{<<"erlando">>, 2 | {git,"https://github.com/rabbitmq/erlando", 3 | {ref,"1c1ef25a9bc228671b32b4a6ee30c7525314b1fd"}}, 4 | 0}]. 5 | -------------------------------------------------------------------------------- /docs/todo.org: -------------------------------------------------------------------------------- 1 | 2 | * haskell 3 | 4 | Нужен удобный запуск в консоли 5 | 6 | 7 | * other 8 | 9 | Версии на других языках: OCaml, Scala, Clojure 10 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | # для сравнения реализовать на хаскеле 3 | 4 | 5 | # посмотреть либы 6 | 7 | https://github.com/fogfish/datum 8 | 9 | 10 | # README 11 | 12 | 13 | # Презентация 14 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 2 | "monad": {:hex, :monad, "1.0.5", "bd02263a8dad0894433ca3283ebb6f71a55799e1cd17bda1e8b2ea9e14eeb9c5", [:mix], [], "hexpm"}} 3 | -------------------------------------------------------------------------------- /src_erl/books_shop.app.src: -------------------------------------------------------------------------------- 1 | {application, books_shop, 2 | [ 3 | {description, "Books shop for cats"}, 4 | {vsn, "1.0.0"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications, [kernel, stdlib]}, 8 | {env, []} 9 | ]}. 10 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | debug_info, 3 | warnings_as_errors, 4 | warn_missing_spec 5 | ]}. 6 | 7 | {src_dirs, ["src_erl"]}. 8 | 9 | {deps, [ 10 | {erlando, {git, "https://github.com/rabbitmq/erlando", 11 | {ref, "1c1ef25a9bc228671b32b4a6ee30c7525314b1fd"}}} 12 | ]}. 13 | 14 | -------------------------------------------------------------------------------- /src_hs/Makefile: -------------------------------------------------------------------------------- 1 | compile: compile4 compile5 2 | 3 | compile4: 4 | ghc -o book_shop_4 -outputdir out Main4.hs 5 | 6 | compile5: 7 | ghc -o book_shop_5 -outputdir out Main5.hs 8 | 9 | main_4: 10 | ./book_shop_4 11 | 12 | main_5: 13 | ./book_shop_5 14 | 15 | clean: 16 | -rm out/* 17 | -rm book_shop_* 18 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BookShopProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :book_shop, 7 | version: "1.0.0", 8 | elixirc_paths: ["src_ex"], 9 | deps: deps() 10 | ] 11 | end 12 | 13 | defp deps do 14 | [ 15 | {:dialyxir, "~> 0.5", only: [:dev]}, 16 | {:monad, "~> 1.0.5"} 17 | ] 18 | end 19 | 20 | end -------------------------------------------------------------------------------- /src_hs/Main5.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import qualified BookShop as BS 4 | import qualified System.Random as SR 5 | 6 | handle_create_order :: BS.JsonData -> Either BS.ValidationError BS.Order 7 | handle_create_order json_data = 8 | do 9 | (cat_name, addr_str, book_strs) <- BS.validate_incoming_data json_data 10 | cat <- BS.validate_cat cat_name 11 | address <- BS.validate_address addr_str 12 | books <- sequence $ map (\(t, a) -> BS.get_book t a) book_strs 13 | Right $ BS.create_order cat address books 14 | 15 | 16 | main :: IO() 17 | main = 18 | do 19 | rand <- SR.randomRIO (1 :: Integer, 2 :: Integer) 20 | let test_data = if rand == 1 then BS.test_data else BS.test_data' 21 | case handle_create_order test_data of 22 | Right order -> print order 23 | Left error -> print error 24 | -------------------------------------------------------------------------------- /src_ex/Main5.ex: -------------------------------------------------------------------------------- 1 | defmodule Main5 do 2 | 3 | @spec main :: {:ok, BookShop.Order.t} | {:error, term} 4 | def main do 5 | BookShop.test_data |> handle_create_order 6 | end 7 | 8 | 9 | @spec handle_create_order(map) :: {:ok, BookShop.Order.t} | {:error, term} 10 | def handle_create_order data0 do 11 | with {:ok, data} <- BookShop.validate_incoming_data(data0), 12 | %{"cat" => cat0, "address" => address0, "books" => books0} = data, 13 | {:ok, cat} <- BookShop.validate_cat(cat0), 14 | {:ok, address} <- BookShop.validate_address(address0), 15 | books1 = Enum.map(books0, 16 | fn %{"title" => title, "author" => author} -> 17 | BookShop.get_book title, author 18 | end), 19 | {:ok, books2} <- Pipeline.sequence(books1), 20 | do: {:ok, BookShop.create_order(cat, address, books2)} 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /src_erl/main_6.erl: -------------------------------------------------------------------------------- 1 | -module(main_6). 2 | -compile({parse_transform, do}). 3 | 4 | -export([main/0]). 5 | 6 | 7 | -spec main() -> {ok, books_shop:order()} | {error, term()}. 8 | main() -> 9 | handle_create_order(books_shop:test_data()). 10 | 11 | 12 | -spec handle_create_order(map()) -> {ok, books_shop:order()} | {error, term()}. 13 | handle_create_order(Data0) -> 14 | do([error_m || 15 | Data <- books_shop:validate_incoming_data(Data0), 16 | #{<<"cat">> := Cat0, <<"address">> := Address0, <<"books">> := Books0} = Data, 17 | Cat <- books_shop:validate_cat(Cat0), 18 | Address <- books_shop:validate_address(Address0), 19 | Books1 = lists:map( 20 | fun(#{<<"title">> := Title, <<"author">> := Author}) -> 21 | books_shop:get_book(Title, Author) 22 | end, 23 | Books0), 24 | Books2 <- pipeline:sequence(Books1), 25 | books_shop:create_order(Cat, Address, Books2) 26 | ]). 27 | 28 | -------------------------------------------------------------------------------- /src_ex/Main6.ex: -------------------------------------------------------------------------------- 1 | defmodule Main6 do 2 | 3 | require Monad.Error 4 | 5 | @spec main :: {:ok, BookShop.Order.t} | {:error, term} 6 | def main do 7 | BookShop.test_data |> handle_create_order 8 | end 9 | 10 | 11 | @spec handle_create_order(map) :: {:ok, BookShop.Order.t} | {:error, term} 12 | def handle_create_order data0 do 13 | Monad.Error.m do 14 | data <- BookShop.validate_incoming_data data0 15 | let %{"cat" => cat0, "address" => address0, "books" => books0} = data 16 | cat <- BookShop.validate_cat cat0 17 | address <- BookShop.validate_address address0 18 | let books1 = Enum.map books0, 19 | fn %{"title" => title, "author" => author} -> 20 | BookShop.get_book title, author 21 | end 22 | books2 <- Pipeline.sequence books1 23 | return BookShop.create_order cat, address, books2 24 | end 25 | end 26 | 27 | end -------------------------------------------------------------------------------- /src_ex/Main3.ex: -------------------------------------------------------------------------------- 1 | defmodule Main3 do 2 | 3 | @spec main :: {:ok, BookShop.Order.t} | {:error, term} 4 | def main do 5 | BookShop.test_data |> handle_create_order 6 | end 7 | 8 | 9 | @spec handle_create_order(map) :: {:ok, BookShop.Order.t} | {:error, term} 10 | def handle_create_order data0 do 11 | try do 12 | data = BookShop.validate_incoming_data_ex data0 13 | %{ 14 | "cat" => cat0, 15 | "address" => address0, 16 | "books" => books0 17 | } = data 18 | cat = BookShop.validate_cat_ex cat0 19 | address = BookShop.validate_address_ex address0 20 | books = Enum.map books0, fn %{"title" => title, "author" => author } -> 21 | BookShop.get_book_ex title, author 22 | end 23 | order = BookShop.create_order cat, address, books 24 | {:ok, order} 25 | catch 26 | {:error, reason} -> {:error, reason} 27 | end 28 | end 29 | 30 | end -------------------------------------------------------------------------------- /src_erl/main_3.erl: -------------------------------------------------------------------------------- 1 | -module(main_3). 2 | 3 | -export([main/0]). 4 | 5 | 6 | -spec main() -> {ok, books_shop:order()} | {error, term()}. 7 | main() -> 8 | handle_create_order(books_shop:test_data()). 9 | 10 | 11 | -spec handle_create_order(map()) -> 12 | {ok, books_shop:order()} | {error, term()}. 13 | 14 | handle_create_order(Data0) -> 15 | try 16 | Data = books_shop:validate_incoming_data_ex(Data0), 17 | #{ 18 | <<"cat">> := Cat0, 19 | <<"address">> := Address0, 20 | <<"books">> := Books0 21 | } = Data, 22 | Cat = books_shop:validate_cat_ex(Cat0), 23 | Address = books_shop:validate_address_ex(Address0), 24 | Books = lists:map( 25 | fun(#{<<"title">> := Title, <<"author">> := Author}) -> 26 | books_shop:get_book_ex(Title, Author) 27 | end, 28 | Books0 29 | ), 30 | Order = books_shop:create_order(Cat, Address, Books), 31 | {ok, Order} 32 | catch 33 | throw:Error -> Error 34 | end. 35 | 36 | -------------------------------------------------------------------------------- /src_erl/wannabe_haskell.erl: -------------------------------------------------------------------------------- 1 | -module(wannabe_haskell). 2 | 3 | -export([do/1]). 4 | 5 | -type step() :: {Key :: atom(), StepFun :: fun(), Args :: term()}. 6 | 7 | -spec do([step()]) -> {ok, term()} | {error, term()}. 8 | do(Steps) -> 9 | State = #{last_result => undefined}, 10 | State2 = lists:foldl(fun do_step/2, State, Steps), 11 | #{last_result := Reply} = State2, 12 | case Reply of 13 | {error, Error} -> {error, Error}; 14 | Res -> {ok, Res} 15 | end. 16 | 17 | 18 | do_step(_, #{last_result := {error, _}} = State) -> State; 19 | 20 | do_step({Key, StepFun, Args}, State) when is_list(Args) -> 21 | Args2 = lists:map( 22 | fun 23 | (Arg) when is_atom(Arg) -> maps:get(Arg, State); 24 | (Arg) -> Arg 25 | end, 26 | Args), 27 | case erlang:apply(StepFun, Args2) of 28 | {ok, Res} -> State#{Key => Res, last_result => Res}; 29 | {error, Error} -> State#{last_result => {error, Error}}; 30 | Res -> State#{Key => Res, last_result => Res} 31 | end; 32 | 33 | do_step({Key, StepFun, Arg}, State) -> 34 | do_step({Key, StepFun, [Arg]}, State). -------------------------------------------------------------------------------- /src_erl/main_5.erl: -------------------------------------------------------------------------------- 1 | -module(main_5). 2 | 3 | -export([main/0]). 4 | 5 | 6 | -spec main() -> {ok, books_shop:order()} | {error, term()}. 7 | main() -> 8 | handle_create_order(books_shop:test_data()). 9 | 10 | 11 | -spec handle_create_order(map()) -> {ok, books_shop:order()} | {error, term()}. 12 | handle_create_order(Data) -> 13 | wannabe_haskell:do([ 14 | {data, fun books_shop:validate_incoming_data/1, Data}, 15 | 16 | {cat_0, fun(#{<<"cat">> := Cat0}) -> {ok, Cat0} end, data}, 17 | {cat, fun books_shop:validate_cat/1, cat_0}, 18 | 19 | {address_0, fun(#{<<"address">> := Address0}) -> {ok, Address0} end, data}, 20 | {address, fun books_shop:validate_address/1, address_0}, 21 | 22 | {books_0, fun(#{<<"books">> := Books0}) -> {ok, Books0} end, data}, 23 | {books, 24 | fun(Books0) -> 25 | pipeline:sequence( 26 | lists:map( 27 | fun(#{<<"title">> := Title, <<"author">> := Author}) -> 28 | books_shop:get_book(Title, Author) 29 | end, 30 | Books0 31 | )) 32 | end, 33 | books_0}, 34 | 35 | {order, fun books_shop:create_order/3, [cat, address, books]} 36 | ]). 37 | 38 | -------------------------------------------------------------------------------- /docs/notes.org: -------------------------------------------------------------------------------- 1 | * compile & run 2 | 3 | ** elixir 4 | 5 | mix compile 6 | iex -S mix 7 | > BookShop.test_data 8 | > Main1.main 9 | 10 | r MyModule 11 | 12 | http://elixirschool.com/ru/lessons/specifics/debugging/ 13 | mix dialyzer 14 | 15 | 16 | ** erlang 17 | 18 | rebar3 compile 19 | rebar3 shell 20 | > main_1:main(). 21 | > main_2:main(). 22 | > main_3:main(). 23 | > main_4:main(). 24 | > main_5:main(). 25 | 26 | 27 | ** haskell 28 | 29 | make compile 30 | make main_4 31 | make main_5 32 | 33 | 34 | * fp notes 35 | 36 | ** monads in erlang 37 | 38 | https://github.com/rabbitmq/erlando 39 | https://github.com/fogfish/datum 40 | https://github.com/yzh44yzh/erlz 41 | 42 | 43 | ** monads in elixir 44 | 45 | With: 46 | https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html#with 47 | https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1 48 | 49 | Monad: 50 | http://www.zohaib.me/monads-in-elixir-2/ 51 | https://hex.pm/packages/monad 52 | https://hexdocs.pm/monad/Monad.Error.html 53 | 54 | 55 | ** Какие вообще есть способы композиции? 56 | 57 | точка, $, & или |> (pipe) 58 | <$>, <*> 59 | >>= bind, >> fmap 60 | =<< 61 | <=< >=> рыба 62 | 63 | В эликсире 64 | только |> 65 | еще есть макрос with 66 | 67 | В эрланге ничего 68 | 69 | Реализация в Erlang работает только для одного типа возвращаемого значения: {ok, _} | {error, _}. 70 | В Haskell это работает для любой монады. 71 | -------------------------------------------------------------------------------- /src_hs/Main4.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import qualified BookShop as BS 4 | import qualified System.Random as SR 5 | 6 | 7 | handle_create_order :: BS.JsonData -> Either BS.ValidationError BS.Order 8 | handle_create_order json_data = 9 | return json_data 10 | >>= validate_cat 11 | >>= validate_address 12 | >>= validate_books 13 | >>= \(c, a, bs) -> Right $ BS.create_order c a bs 14 | 15 | 16 | validate_cat :: BS.JsonData -> Either BS.ValidationError (BS.JsonData, BS.Cat) 17 | validate_cat json_data = 18 | let (cat, _, _) = json_data in 19 | BS.validate_cat cat 20 | >>= \cat -> Right (json_data, cat) 21 | 22 | 23 | validate_address :: (BS.JsonData, BS.Cat) -> Either BS.ValidationError (BS.JsonData, BS.Cat, BS.Address) 24 | validate_address (json_data, cat) = 25 | let (_, address, _) = json_data in 26 | BS.validate_address "Coolcat str 7/42 Minsk Belarus" 27 | >>= \addr -> Right (json_data, cat, addr) 28 | 29 | 30 | validate_books :: (BS.JsonData, BS.Cat, BS.Address) -> Either BS.ValidationError (BS.Cat, BS.Address, [BS.Book]) 31 | validate_books (json_data, cat, addr) = 32 | get_books book_str >>= \books -> Right (cat, addr, books) 33 | where 34 | (_, _, book_str) = json_data 35 | get_books bs = sequence $ map (\(t, a) -> BS.get_book t a) bs 36 | 37 | 38 | main :: IO() 39 | main = 40 | do 41 | rand <- SR.randomRIO (1 :: Integer, 2 :: Integer) 42 | let test_data = if rand == 1 then BS.test_data else BS.test_data' 43 | case handle_create_order test_data of 44 | Right order -> print order 45 | Left error -> print error 46 | -------------------------------------------------------------------------------- /src_ex/Pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Pipeline do 2 | 3 | @type result :: {:ok, term} | {:error, term} 4 | @type pipeline_fun :: (term -> result) 5 | 6 | @spec bind(term, [pipeline_fun]) :: result 7 | def bind arg, funs do 8 | Enum.reduce funs, {:ok, arg}, 9 | fn 10 | f, {:ok, prev_res} -> f.(prev_res) 11 | _, {:error, reason} -> {:error, reason} 12 | end 13 | end 14 | 15 | 16 | @spec sequence([result]) :: result 17 | def sequence [] do {:ok, []} end 18 | def sequence [{:error, reason} | _] do {:error, reason} end 19 | def sequence [{:ok, val} | tail] do 20 | case sequence tail do 21 | {:ok, list} -> {:ok, [val | list]} 22 | {:error, error} -> {:error, error} 23 | end 24 | end 25 | 26 | 27 | @spec sample(integer) :: result 28 | def sample arg do 29 | bind arg, [ 30 | fn a -> {:ok, a + 10} end, 31 | fn a -> 32 | if a > 0 do {:ok, a * 2} 33 | else {:error, {:invalid_arg, a}} 34 | end 35 | end, 36 | fn a -> {:ok, [a, a, a]} end, 37 | fn a_list -> Enum.map a_list, fn a -> {a, a + 1} end end 38 | ] 39 | end 40 | 41 | 42 | def sample2 do 43 | list1 = [{:ok, 1}, {:ok, 2}, {:ok, 3}, {:ok, 4}] 44 | res1 = sequence list1 45 | IO.puts "list1: #{inspect list1}, res1: #{inspect res1}" 46 | 47 | list2 = [{:ok, 1}, {:error, :something_wrong_2}, {:error, :something_wrong_3}, {:ok, 4}] 48 | res2 = sequence list2 49 | IO.puts "list2: #{inspect list2}, res2: #{inspect res2}" 50 | end 51 | 52 | end -------------------------------------------------------------------------------- /src_ex/Main1.ex: -------------------------------------------------------------------------------- 1 | defmodule Main1 do 2 | 3 | @spec main :: {:ok, BookShop.Order.t} | {:error, term} 4 | def main do 5 | BookShop.test_data |> handle_create_order 6 | end 7 | 8 | 9 | @spec handle_create_order(map) :: {:ok, BookShop.Order.t} | {:error, term} 10 | def handle_create_order data do 11 | case BookShop.validate_incoming_data data do 12 | {:error, reason} -> {:error, reason} 13 | {:ok, %{ 14 | "cat" => cat0, 15 | "address" => address0, 16 | "books" => books0 17 | }} -> 18 | case BookShop.validate_cat cat0 do 19 | {:error, reason} -> {:error, reason} 20 | {:ok, cat} -> 21 | case BookShop.validate_address address0 do 22 | {:error, reason} -> {:error, reason} 23 | {:ok, address} -> 24 | books1 = Enum.map books0, 25 | fn %{"title" => title, "author" => author} -> 26 | BookShop.get_book title, author 27 | end 28 | invalid_books = Enum.filter books1, fn {res, _} -> res == :error end 29 | case invalid_books do 30 | [{:error, reason} | _] -> {:error, reason} 31 | [] -> 32 | books2 = Enum.map books1, fn {:ok, book} -> book end 33 | BookShop.create_order cat, address, books2 34 | end 35 | end 36 | end 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /src_erl/main_4.erl: -------------------------------------------------------------------------------- 1 | -module(main_4). 2 | 3 | 4 | -export([main/0]). 5 | 6 | 7 | -spec main() -> {ok, books_shop:order()} | {error, term()}. 8 | main() -> 9 | handle_create_order(books_shop:test_data()). 10 | 11 | 12 | -spec handle_create_order(map()) -> {ok, books_shop:order()} | {error, term()}. 13 | handle_create_order(Data) -> 14 | pipeline:bind(Data, [ 15 | fun books_shop:validate_incoming_data/1, 16 | fun validate_cat/1, 17 | fun validate_address/1, 18 | fun validate_books/1, 19 | fun create_order/1 20 | ]). 21 | 22 | 23 | -spec validate_cat(map()) -> {ok, map()} | {error, term()}. 24 | validate_cat(#{<<"cat">> := Cat0} = State) -> 25 | case books_shop:validate_cat(Cat0) of 26 | {ok, Cat} -> {ok, State#{cat => Cat}}; 27 | Error -> Error 28 | end. 29 | 30 | 31 | -spec validate_address(map()) -> {ok, map()} | {error, term()}. 32 | validate_address(#{<<"address">> := Address0} = State) -> 33 | case books_shop:validate_address(Address0) of 34 | {ok, Address} -> {ok, State#{address => Address}}; 35 | Error -> Error 36 | end. 37 | 38 | 39 | -spec validate_books(map()) -> {ok, map()} | {error, term()}. 40 | validate_books(#{<<"books">> := Books0} = State) -> 41 | Books1 = lists:map( 42 | fun(#{<<"title">> := Title, <<"author">> := Author}) -> 43 | books_shop:get_book(Title, Author) 44 | end, 45 | Books0 46 | ), 47 | case pipeline:sequence(Books1) of 48 | {ok, Books2} -> {ok, State#{books => Books2}}; 49 | Error -> Error 50 | end. 51 | 52 | 53 | -spec create_order(map()) -> {ok, books_shop:order()}. 54 | create_order(#{cat := Cat, address := Address, books := Books}) -> 55 | Order = books_shop:create_order(Cat, Address, Books), 56 | {ok, Order}. 57 | -------------------------------------------------------------------------------- /src_ex/Main4.ex: -------------------------------------------------------------------------------- 1 | defmodule Main4 do 2 | 3 | @spec main :: {:ok, BookShop.Order.t} | {:error, term} 4 | def main do 5 | BookShop.test_data |> handle_create_order 6 | end 7 | 8 | 9 | @spec handle_create_order(map) :: {:ok, BookShop.Order.t} | {:error, term} 10 | def handle_create_order data0 do 11 | Pipeline.bind data0, [ 12 | &BookShop.validate_incoming_data/1, 13 | &validate_cat/1, 14 | &validate_address/1, 15 | &validate_books/1, 16 | &create_order/1 17 | ] 18 | end 19 | 20 | 21 | @spec validate_cat(map) :: {:ok, map} | {:error, term} 22 | def validate_cat %{"cat" => cat0} = state do 23 | case BookShop.validate_cat cat0 do 24 | {:ok, cat} -> {:ok, (Map.put state, :cat, cat)} 25 | error -> error 26 | end 27 | end 28 | 29 | 30 | @spec validate_address(map) :: {:ok, map} | {:error, term} 31 | def validate_address %{"address" => address0} = state do 32 | case BookShop.validate_address address0 do 33 | {:ok, address} -> {:ok, (Map.put state, :address, address)} 34 | error -> error 35 | end 36 | end 37 | 38 | 39 | @spec validate_books(map) :: {:ok, map} | {:error, term} 40 | def validate_books %{"books" => books0} = state do 41 | books1 = Enum.map books0, 42 | fn %{"title" => title, "author" => author} -> 43 | BookShop.get_book title, author 44 | end 45 | case Pipeline.sequence books1 do 46 | {:ok, books2} -> {:ok, (Map.put state, :books, books2)} 47 | error -> error 48 | end 49 | end 50 | 51 | 52 | @spec create_order(map) :: {:ok, BookShop.Order.t} 53 | def create_order %{:cat => cat, :address => address, :books => books} do 54 | order = BookShop.create_order cat, address, books 55 | {:ok, order} 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /src_hs/BookShop.hs: -------------------------------------------------------------------------------- 1 | module BookShop where 2 | 3 | import qualified System.Random as SR 4 | 5 | data Cat = Cat String deriving Show 6 | 7 | data Address = Address String deriving Show 8 | 9 | data Book = 10 | Book { id :: String 11 | , title :: String 12 | , author :: String 13 | } deriving Show 14 | 15 | data Order = 16 | Order { customer :: Cat 17 | , shipping_address :: Address 18 | , books :: [Book] 19 | } deriving Show 20 | 21 | data ValidationError 22 | = InvalidIncomingData 23 | | CatNotFound 24 | | InvalidAddress 25 | | BookNotFound 26 | deriving Show 27 | 28 | 29 | type JsonData = (String, String, [(String, String)]) 30 | 31 | test_data :: JsonData 32 | test_data = 33 | ("Tihon", "Coolcat str 7/42 Minsk Belarus", 34 | [ ("Scott Wlaschin", "Domain Modeling Made Functional") 35 | , ("Стивен Строгац", "Удовольствие от Х") 36 | , ("Mikito Takada", "Distributed systems for fun and profit") 37 | ]) 38 | 39 | 40 | test_data' :: JsonData 41 | test_data' = 42 | ("Marfa", "Coolcat str 7/42 Minsk Belarus", 43 | [ ("Scott Wlaschin", "Domain Modeling Made Functional") 44 | , ("Mikito Takada", "Distributed systems for fun and profit") 45 | ]) 46 | 47 | 48 | validate_incoming_data :: JsonData -> Either ValidationError JsonData 49 | validate_incoming_data json_data = 50 | Right json_data 51 | 52 | 53 | validate_cat :: String -> Either ValidationError Cat 54 | validate_cat cat_name = 55 | case cat_name of 56 | "Tihon" -> Right $ Cat cat_name 57 | _ -> Left CatNotFound 58 | 59 | 60 | validate_address :: String -> Either ValidationError Address 61 | validate_address addr_str = 62 | Right $ Address addr_str 63 | 64 | 65 | get_book :: String -> String -> Either ValidationError Book 66 | get_book title author = 67 | Right $ Book "ISBN 978-5-00057-917-6" title author 68 | 69 | 70 | create_order :: Cat -> Address -> [Book] -> Order 71 | create_order cat address books = 72 | Order cat address books 73 | -------------------------------------------------------------------------------- /src_erl/main_1.erl: -------------------------------------------------------------------------------- 1 | -module(main_1). 2 | 3 | -export([main/0]). 4 | 5 | 6 | -spec main() -> 7 | {ok, books_shop:order()} | {error, term()}. 8 | 9 | main() -> 10 | handle_create_order(books_shop:test_data()). 11 | 12 | 13 | -spec handle_create_order(map()) -> 14 | {ok, books_shop:order()} | {error, term()}. 15 | 16 | handle_create_order(Data0) -> 17 | case books_shop:validate_incoming_data(Data0) of 18 | {error, Reason} -> {error, Reason}; 19 | {ok, #{ 20 | <<"cat">> := Cat0, 21 | <<"address">> := Address0, 22 | <<"books">> := Books0 23 | }} -> 24 | case books_shop:validate_cat(Cat0) of 25 | {error, Reason} -> {error, Reason}; 26 | {ok, Cat} -> 27 | case books_shop:validate_address(Address0) of 28 | {error, Reason} -> {error, Reason}; 29 | {ok, Address} -> 30 | Books1 = lists:map( 31 | fun(#{<<"title">> := Title, <<"author">> := Author}) -> 32 | books_shop:get_book(Title, Author) 33 | end, 34 | Books0 35 | ), 36 | InvalidBooks = lists:filter( 37 | fun 38 | ({error, _}) -> true; 39 | ({ok, _}) -> false 40 | end, 41 | Books1 42 | ), 43 | case InvalidBooks of 44 | [Error | _] -> Error; 45 | [] -> 46 | Books2 = lists:map( 47 | fun({ok, Book}) -> Book end, 48 | Books1 49 | ), 50 | books_shop:create_order(Cat, Address, Books2) 51 | end 52 | end 53 | end 54 | end. 55 | -------------------------------------------------------------------------------- /src_erl/pipeline.erl: -------------------------------------------------------------------------------- 1 | -module(pipeline). 2 | 3 | -export_type([result/0, pipeline_fun/0]). 4 | 5 | -export([ 6 | bind/2, 7 | sequence/1, 8 | sample/1, 9 | sample_2/0 10 | ]). 11 | 12 | 13 | %%% Types 14 | 15 | -type result() :: {ok, term()} | {error, term()}. 16 | 17 | -type pipeline_fun() :: fun((term()) -> result()). 18 | 19 | %% This doesn't help: 20 | %% 21 | %% -type result(Ok, Error) :: {ok, Ok} | {error, Error}. 22 | %% 23 | %% -type pipeline_fun(InArg, OkRes, ErrRes) :: fun((InArg) -> result(OkRes, ErrRes)). 24 | %% 25 | %% -spec sample(integer()) -> 26 | %% result( 27 | %% [{integer(), integer()}], 28 | %% {atom(), integer()} 29 | %% ). 30 | 31 | 32 | %%% Module API 33 | 34 | -spec bind(term(), [pipeline_fun()]) -> result(). 35 | bind(Arg, Funs) -> 36 | lists:foldl( 37 | fun 38 | (Fun, {ok, PrevRes}) -> Fun(PrevRes); 39 | (_, {error, Error}) -> {error, Error} 40 | end, 41 | {ok, Arg}, 42 | Funs). 43 | 44 | 45 | -spec sequence([result()]) -> result(). 46 | sequence([]) -> {ok, []}; 47 | sequence([{error, Error} | _]) -> {error, Error}; 48 | sequence([{ok, Value} | Tail]) -> 49 | case sequence(Tail) of 50 | {ok, List} -> {ok, [Value | List]}; 51 | {error, Error} -> {error, Error} 52 | end. 53 | 54 | 55 | %%% Samples 56 | 57 | -spec sample(integer()) -> result(). 58 | sample(In) -> 59 | bind(In, [ 60 | fun(A) -> {ok, A + 10} end, 61 | fun(A) -> 62 | if 63 | A > 0 -> {ok, A * 2}; 64 | %% A > 0 -> {ok, <<"some">>}; % dialyzer can't find this 65 | A =< 0 -> {error, {invalid_arg, A}} 66 | end 67 | end, 68 | fun(A) -> {ok, [A, A, A]} end, 69 | fun(AList) -> {ok, lists:map(fun(A) -> {A, A+1} end, AList)} end 70 | ]). 71 | 72 | 73 | -spec sample_2() -> ok. 74 | sample_2() -> 75 | List1 = [{ok, 1}, {ok, 2}, {ok, 3}, {ok, 4}], 76 | Res1 = sequence(List1), 77 | io:format("List1:~p, Res1:~p~n", [List1, Res1]), 78 | 79 | List2 = [{ok, 1}, {error, something_wrong_2}, {error, something_wrong_3}, {ok, 4}], 80 | Res2 = sequence(List2), 81 | io:format("List2:~p, Res2:~p~n", [List2, Res2]), 82 | ok. -------------------------------------------------------------------------------- /src_ex/Main2.ex: -------------------------------------------------------------------------------- 1 | defmodule Main2 do 2 | 3 | @spec main :: {:ok, BookShop.Order.t} | {:error, term} 4 | def main do 5 | data = BookShop.test_data 6 | handle_create_order data, %{} 7 | end 8 | 9 | 10 | @spec handle_create_order(map, map) :: {:ok, BookShop.Order.t} | {:error, term} 11 | def handle_create_order data0, state do 12 | case BookShop.validate_incoming_data data0 do 13 | {:error, reason} -> {:error, reason} 14 | {:ok, data} -> validate_cat data, state 15 | end 16 | end 17 | 18 | 19 | @spec validate_cat(map, map) :: {:ok, BookShop.Order.t} | {:error, term} 20 | def validate_cat %{"cat" => cat0} = data, state0 do 21 | case BookShop.validate_cat cat0 do 22 | {:error, reason} -> {:error, reason} 23 | {:ok, cat} -> 24 | state = Map.put state0, :cat, cat 25 | validate_address data, state 26 | end 27 | end 28 | 29 | 30 | @spec validate_address(map, map) :: {:ok, BookShop.Order.t} | {:error, term} 31 | def validate_address %{"address" => address0} = data, state0 do 32 | case BookShop.validate_address address0 do 33 | {:error, reason} -> {:error, reason} 34 | {:ok, address} -> 35 | state = Map.put state0, :address, address 36 | validate_books data, state 37 | end 38 | end 39 | 40 | 41 | @spec validate_books(map, map) :: {:ok, BookShop.Order.t} | {:error, term} 42 | def validate_books %{"books" => books0}, state0 do 43 | books = Enum.map books0, 44 | fn %{"title" => title, "author" => author} -> 45 | BookShop.get_book title, author 46 | end 47 | invalid_books = Enum.filter books, fn {res, _} -> res == :error end 48 | case invalid_books do 49 | [{:error, reason} | _] -> {:error, reason} 50 | [] -> 51 | state = Map.put state0, :books, books 52 | create_order state 53 | end 54 | end 55 | 56 | 57 | @spec create_order(map) :: {:ok, BookShop.Order.t} 58 | def create_order %{:cat => cat, :address => address, :books => books0} do 59 | books = Enum.map books0, fn {:ok, book} -> book end 60 | order = BookShop.create_order cat, address, books 61 | {:ok, order} 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /src_erl/main_2.erl: -------------------------------------------------------------------------------- 1 | -module(main_2). 2 | 3 | -export([main/0]). 4 | 5 | 6 | -spec main() -> 7 | {ok, books_shop:order()} | {error, term()}. 8 | 9 | main() -> 10 | handle_create_order(books_shop:test_data(), #{}). 11 | 12 | 13 | -spec handle_create_order(map(), map()) -> 14 | {ok, books_shop:order()} | {error, term()}. 15 | 16 | handle_create_order(Data0, State) -> 17 | case books_shop:validate_incoming_data(Data0) of 18 | {error, Reason} -> {error, Reason}; 19 | {ok, Data} -> validate_cat(Data, State) 20 | end. 21 | 22 | 23 | -spec validate_cat(map(), map()) -> 24 | {ok, books_shop:order()} | {error, term()}. 25 | 26 | validate_cat(#{<<"cat">> := Cat0} = Data, State) -> 27 | case books_shop:validate_cat(Cat0) of 28 | {error, Reason} -> {error, Reason}; 29 | {ok, Cat} -> validate_address(Data, State#{cat => Cat}) 30 | end. 31 | 32 | 33 | -spec validate_address(map(), map()) -> 34 | {ok, books_shop:order()} | {error, term()}. 35 | 36 | validate_address(#{<<"address">> := Address0} = Data, State) -> 37 | case books_shop:validate_address(Address0) of 38 | {error, Reason} -> {error, Reason}; 39 | {ok, Address} -> check_books(Data, State#{address => Address}) 40 | end. 41 | 42 | 43 | -spec check_books(map(), map()) -> 44 | {ok, books_shop:order()} | {error, term()}. 45 | 46 | check_books(#{<<"books">> := Books0}, State) -> 47 | Books = lists:map( 48 | fun(#{<<"title">> := Title, <<"author">> := Author}) -> 49 | books_shop:get_book(Title, Author) 50 | end, 51 | Books0 52 | ), 53 | validate_books(Books, State). 54 | 55 | 56 | -spec validate_books(list(), map()) -> 57 | {ok, books_shop:order()} | {error, term()}. 58 | 59 | validate_books(Books, State) -> 60 | InvalidBooks = lists:filter( 61 | fun 62 | ({error, _}) -> true; 63 | ({ok, _}) -> false 64 | end, 65 | Books 66 | ), 67 | case InvalidBooks of 68 | [Error | _] -> Error; 69 | [] -> create_order(State#{books => Books}) 70 | end. 71 | 72 | 73 | -spec create_order(map()) -> 74 | {ok, books_shop:order()}. 75 | 76 | create_order(#{cat := Cat, address := Address, books := Books0}) -> 77 | Books = lists:map( 78 | fun({ok, Book}) -> Book end, 79 | Books0 80 | ), 81 | Order = books_shop:create_order(Cat, Address, Books), 82 | {ok, Order}. 83 | -------------------------------------------------------------------------------- /src_ex/BookShop.ex: -------------------------------------------------------------------------------- 1 | defmodule BookShop do 2 | 3 | # Types 4 | 5 | @type cat :: {:cat, binary} 6 | @type address :: {:address, binary} 7 | 8 | defmodule Book do 9 | defstruct [:id, :title, :author] 10 | @type t :: %Book{id: binary, title: binary, author: binary} 11 | end 12 | 13 | defmodule Order do 14 | alias BookShop, as: BS 15 | defstruct [:customer, :shipping_address, :books] 16 | @type t :: %Order{customer: BS.cat, shipping_address: BS.address, books: [Book.t]} 17 | end 18 | 19 | 20 | # Test Data 21 | 22 | @spec test_data() :: map 23 | def test_data do 24 | %{ 25 | "cat" => "Tihon", 26 | "address" => "Coolcat str 7/42 Minsk Belarus", 27 | "books" => [ 28 | %{ 29 | "title" => "Domain Modeling Made Functional", 30 | "author" => "Scott Wlaschin" 31 | }, 32 | %{ 33 | "title" => "Удовольствие от Х", 34 | "author" => "Стивен Строгац" 35 | }, 36 | %{ 37 | "title" => "Distributed systems for fun and profit", 38 | "author" => "Mikito Takada" 39 | } 40 | ] 41 | } 42 | end 43 | 44 | 45 | # Module API 46 | 47 | @spec validate_incoming_data(map) :: {:ok, map} | {:error, :invalid_incoming_data} 48 | def validate_incoming_data json_data do 49 | case rand_success() do 50 | true -> {:ok, json_data} 51 | false -> {:error, :invalid_incoming_data} 52 | end 53 | end 54 | 55 | 56 | @spec validate_cat(binary) :: {:ok, cat} | {:error, :cat_not_found} 57 | def validate_cat cat_name do 58 | case rand_success() do 59 | true -> {:ok, {:cat, cat_name}} 60 | false -> {:error, :cat_not_found} 61 | end 62 | end 63 | 64 | 65 | @spec validate_address(binary) :: {:ok, address} | {:error, :invalid_address} 66 | def validate_address address do 67 | case rand_success() do 68 | true -> {:ok, {:address, address}} 69 | false -> {:error, :invalid_address} 70 | end 71 | end 72 | 73 | 74 | @spec get_book(binary, binary) :: {:ok, Book.t} | {:error, {:book_not_found, binary}} 75 | def get_book title, author do 76 | case rand_success() do 77 | true -> {:ok, 78 | %Book { 79 | id: "ISBN 978-5-00057-917-6", 80 | title: title, 81 | author: author 82 | } 83 | } 84 | false -> {:error, {:book_not_found, title}} 85 | end 86 | end 87 | 88 | 89 | @spec create_order(cat, address, [Book.t]) :: Order.t 90 | def create_order cat, address, books do 91 | %Order { customer: cat, shipping_address: address, books: books } 92 | end 93 | 94 | 95 | @spec validate_incoming_data_ex(map) :: map 96 | def validate_incoming_data_ex json_data do 97 | case rand_success() do 98 | true -> json_data 99 | false -> throw {:error, :invalid_incoming_data} 100 | end 101 | end 102 | 103 | 104 | @spec validate_cat_ex(binary) :: cat 105 | def validate_cat_ex cat_name do 106 | case rand_success() do 107 | true -> {:cat, cat_name} 108 | false -> throw {:error, :cat_not_found} 109 | end 110 | end 111 | 112 | 113 | @spec validate_address_ex(binary) :: address 114 | def validate_address_ex address do 115 | case rand_success() do 116 | true -> {:address, address} 117 | false -> throw {:error, :invalid_address} 118 | end 119 | end 120 | 121 | 122 | @spec get_book_ex(binary, binary) :: Book.t 123 | def get_book_ex title, author do 124 | case rand_success() do 125 | true -> 126 | %Book { 127 | id: "ISBN 978-5-00057-917-6", 128 | title: title, 129 | author: author 130 | } 131 | false -> throw {:error, {:book_not_found, title}} 132 | end 133 | end 134 | 135 | 136 | # Internal functions 137 | 138 | @spec rand_success() :: boolean 139 | def rand_success do 140 | rand = :rand.uniform 10 141 | rand > 1 142 | end 143 | 144 | end 145 | -------------------------------------------------------------------------------- /src_erl/books_shop.erl: -------------------------------------------------------------------------------- 1 | -module(books_shop). 2 | 3 | -export_type([cat/0, address/0, book/0, order/0]). 4 | 5 | -export([ 6 | test_data/0, 7 | validate_incoming_data/1, 8 | validate_cat/1, 9 | validate_address/1, 10 | get_book/2, 11 | create_order/3, 12 | validate_incoming_data_ex/1, 13 | validate_cat_ex/1, 14 | validate_address_ex/1, 15 | get_book_ex/2 16 | ]). 17 | 18 | 19 | %%% Types 20 | 21 | -type cat() :: {cat, binary()}. 22 | 23 | -type address() :: {address, binary()}. 24 | 25 | -record(book, { 26 | id :: binary(), 27 | title :: binary(), 28 | author :: binary() 29 | }). 30 | -type book() :: #book{}. 31 | 32 | -record(order, { 33 | customer :: cat(), 34 | shipping_address :: address(), 35 | books :: [book()] 36 | }). 37 | -type order() :: #order{}. 38 | 39 | 40 | %%% Test Data 41 | 42 | -spec test_data() -> map(). 43 | test_data() -> 44 | #{ 45 | <<"cat">> => <<"Tihon">>, 46 | <<"address">> => <<"Coolcat str 7/42 Minsk Belarus">>, 47 | <<"books">> => [ 48 | #{ 49 | <<"title">> => <<"Domain Modeling Made Functional">>, 50 | <<"author">> => <<"Scott Wlaschin">> 51 | }, 52 | #{ 53 | <<"title">> => <<"Удовольствие от Х"/utf8>>, 54 | <<"author">> => <<"Стивен Строгац"/utf8>> 55 | }, 56 | #{ 57 | <<"title">> => <<"Distributed systems for fun and profit">>, 58 | <<"author">> => <<"Mikito Takada">> 59 | } 60 | ] 61 | }. 62 | 63 | 64 | %%% Module API 65 | 66 | -spec validate_incoming_data(JsonData :: map()) -> 67 | {ok, map()} | {error, invalid_incoming_data}. 68 | 69 | validate_incoming_data(JsonData) -> 70 | %% jesse:validate("order", JsonData) 71 | case rand_success() of 72 | true -> {ok, JsonData}; 73 | false -> {error, invalid_incoming_data} 74 | end. 75 | 76 | 77 | -spec validate_cat(CatName :: binary()) -> 78 | {ok, cat()} | {error, cat_not_found}. 79 | 80 | validate_cat(CatName) -> 81 | %% find cat in database 82 | case rand_success() of 83 | true -> {ok, {cat, CatName}}; 84 | false -> {error, cat_not_found} 85 | end. 86 | 87 | 88 | -spec validate_address(Address :: binary()) -> 89 | {ok, address()} | {error, invalid_address}. 90 | 91 | validate_address(Address) -> 92 | %% send request to third-party service 93 | case rand_success() of 94 | true -> {ok, {address, Address}}; 95 | false -> {error, invalid_address} 96 | end. 97 | 98 | 99 | -spec get_book(Title :: binary(), Author :: binary()) -> 100 | {ok, book()} | {error, {book_not_found, binary()}}. 101 | 102 | get_book(Title, Author) -> 103 | %% find book in database 104 | case rand_success() of 105 | true -> 106 | {ok, #book{ 107 | id = <<"ISBN 978-5-00057-917-6">>, 108 | title = Title, 109 | author = Author 110 | }}; 111 | false -> {error, {book_not_found, Title}} 112 | end. 113 | 114 | 115 | -spec create_order(Cat :: cat(), Address :: address(), Books :: [book()]) -> 116 | order(). 117 | 118 | create_order(Cat, Address, Books) -> 119 | #order{ 120 | customer = Cat, 121 | shipping_address = Address, 122 | books = Books 123 | }. 124 | 125 | 126 | %%% Module API with Exceptions 127 | 128 | -spec validate_incoming_data_ex(JsonData :: map()) -> map(). 129 | validate_incoming_data_ex(JsonData) -> 130 | %% jesse:validate("order", JsonData) 131 | case rand_success() of 132 | true -> JsonData; 133 | false -> throw({error, invalid_incoming_data}) 134 | end. 135 | 136 | 137 | -spec validate_cat_ex(CatName :: binary()) -> cat(). 138 | validate_cat_ex(CatName) -> 139 | %% find cat in database 140 | case rand_success() of 141 | true -> {cat, CatName}; 142 | false -> throw({error, cat_not_found}) 143 | end. 144 | 145 | 146 | -spec validate_address_ex(Address :: binary()) -> address(). 147 | validate_address_ex(Address) -> 148 | %% send request to third-party service 149 | case rand_success() of 150 | true -> {address, Address}; 151 | false -> throw({error, invalid_address}) 152 | end. 153 | 154 | 155 | -spec get_book_ex(Title :: binary(), Author :: binary()) -> book(). 156 | get_book_ex(Title, Author) -> 157 | %% find book in database 158 | case rand_success() of 159 | true -> 160 | #book{ 161 | id = <<"ISBN 978-5-00057-917-6">>, 162 | title = Title, 163 | author = Author 164 | }; 165 | false -> throw({error, {book_not_found, Title}}) 166 | end. 167 | 168 | 169 | %%% Internal functions 170 | 171 | -spec rand_success() -> boolean(). 172 | rand_success() -> 173 | Rand = rand:uniform(10), 174 | Rand > 1. 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | В данном учебном проекте я рассматриваю способы композиции функций в Elixir и Erlang. 2 | 3 | На примере конкретной прикладной задачи я предлагаю 5 вариантов композиции, начиная с простых и понятных, двигаюсь к более сложным способам, характерным для функционального программирования. Анализирую плюсы и минусы каждого варианта, рекомендую, что лучше использовать в реальном проекте. 4 | 5 | 6 | # Задача 7 | 8 | У нас есть книжный магазин для котов. Он принимает заказы и доставляет книги. 9 | 10 | У магазина есть API для создания заказа. 11 | 12 | На входе API принимает json-данные, содержащие информацию о коте-заказчике, его адрес, и книги, которые кот хочет заказать. 13 | 14 | Например: 15 | ``` 16 | { 17 | "cat": "Tihon", 18 | "address": "Coolcat str 7/42 Minsk Belarus", 19 | "books": [ 20 | {"title": "Domain Modeling Made Functional", "author": "Scott Wlaschin"}, 21 | {"title": "Удовольствие от Х", "author": "Стивен Строгац"}, 22 | {"title": "Distributed systems for fun and profit", "author": "Mikito Takada"} 23 | ] 24 | } 25 | ``` 26 | 27 | На выходе из API мы имеем валидный бизнес-объект **Order**, который передается дальше в систему для обработки, или ошибку валидации. 28 | 29 | Для проверки данных и создания валидного объекта Order нужно выполнить следующие шаги: 30 | - проверить кота по имени 31 | - проверить его адрес 32 | - проверить каждую книгу в списке 33 | - создать Order 34 | 35 | Для этого у нас есть следующие функции: 36 | ``` 37 | @spec validate_incoming_data(map) :: {:ok, map} | {:error, :invalid_incoming_data} 38 | 39 | @spec validate_cat(binary) :: {:ok, cat} | {:error, :cat_not_found} 40 | 41 | @spec validate_address(binary) :: {:ok, address} | {:error, :invalid_address} 42 | 43 | @spec get_book(binary, binary) :: {:ok, Book.t} | {:error, {:book_not_found, binary}} 44 | 45 | @spec create_order(cat, address, [Book.t]) :: Order.t 46 | ``` 47 | 48 | То есть, у нас есть 3 функции, которые могут вернуть успешный результат, либо ошибку. Четвертая функция, которую нужно применить несколько раз к элементам списка. И, наконец, пятая функция, которая всегда возвращает успешный результат. 49 | 50 | Нужно выполнить композицию этих функций. 51 | 52 | [BookShop.ex](./src_ex/BookShop.ex) 53 | 54 | 55 | # Вариант 1. Решение в лоб -- вложенные case. 56 | 57 | [Main1.ex](./src_ex/Main1.ex) 58 | 59 | Здесь получилось 4 уровня вложенности. Пока что это не так страшно. Но что, если понадобится добавить еще один шаг валидации? Два шага? Десять? Или переставить некоторые шаги местами? 60 | 61 | Такой код -- явный пример, как не надо делать. Тем не менее, тут есть пару плюсов. Во-первых, он работает. Во-вторых, он хорошо проверяется dialyzer. 62 | 63 | Я не поленился создать пользовательские типы данных и написать spec ко всем функциям. Так что теперь dialyzer может проверить правильность композиции функций. 64 | 65 | 66 | # Вариант 2. Каждый case в отдельную функцию. 67 | 68 | [Main2.ex](./src_ex/Main2.ex) 69 | 70 | У нас получилось 5 небольших функций, вызывающих друг друга по очереди. И некий общий **State**, который проходит через все эти вызовы. State нужен, чтобы накапливать промежуточные результаты и передавать их дальше. 71 | 72 | Каждая функция маленькая и понятная. Тут легко добавить два, пять, десять, сколько угодно новых шагов валидации. Легко менять их местами. 73 | 74 | dialyzer по-прежнему контролирует правильность композиции. Но за правильностью использования State разработчику придется следить самому. Тут появляются возможности для ошибок. 75 | 76 | Кроме того, функции похожи, они повторяют одинаковый шаблон. И это наводит на мысль, что можно что-то обобщить, сократить количество кода. 77 | 78 | 79 | # Вариант 3. Решение с использованием исключений. 80 | 81 | [Main3.ex](./src_ex/Main3.ex) 82 | 83 | Шаблонность кода вызвана тем, что результат каждого вызова нужно проверить на ошибку. Попробуем переделать модуль BookShop, чтобы его функции сообщали об ошибках через исключения, а не через возвращаемое значение. 84 | 85 | Получилось очень просто, лаконично. Код пишем только для happy path, **try..catch** решает все остальные проблемы. Красота! 86 | 87 | В таком простом примере это решение может показаться самым лучшим. Но в больших проектах исключения создают некоторые проблемы. 88 | 89 | Во-первых, понадобится много разных типов исключений и стратегия их использования -- какой тип для чего применять. Если разработчиков на проекте больше одного, то таких стратегий может оказаться больше одной. И тогда неизвестные исключения вдруг прилетают из неожиданных мест. 90 | 91 | Во-вторых, вызывая функцию, разработчик не может знать всех возможных вариантов ее завершения. Разве что прочитает ее код и проследит внутренние вызовы на всю глубину. В решениях 1 и 2 все возможные варианты завершения функции описаны в ее спецификации. По-хорошему, спецификация могла бы содержать информацию, какие исключения могут возникнуть (как в Java). Но в Erlang этого нет. 92 | 93 | В использовании исключений ничего плохого нет (даже если некоторые ФП программисты будут говорить вам обратное). Но в мире Erlang исключения не очень популярны. 94 | 95 | 96 | # Вариант 4. Pipeline, bind и sequence. 97 | 98 | Исключения дали нам возможность сосредоточиться на happy path и не мучиться с шаблонной обработкой ошибок. В функциональном программировании есть другие инструменты с таким же эффектом. 99 | 100 | Сначала пару слов про Haskell. Дело в том, что Haskell может показать нужные нам идеи в эталонном виде. А в Elixir/Erlang мы может реализовать только что-то похожее, с некоторым приближением. Так что прежде, чем смотреть на искаженную копию, сперва посмотрим на оригинал. 101 | 102 | Нам нужны те же функции валидации, реализованные на Haskell: [BookShop.hs](./src_hs/BookShop.hs) 103 | 104 | Тип данных **Either ErrResult SuccessResult** -- это аналог нашего **{:ok, success_result} | {:error, error_result}**. 105 | 106 | Теперь мы будем соединять эти функции в цепочку. Это легко, когда результат одной функции совпадает с аргументом другой. Но что делать, если не совпадает? Собственно, вокруг этого и строится все ФП :) 107 | 108 | Если на выходе из функции нужное нам значение завернуто в Either, а на входе другой функции это значение нужно в чистом виде, то соединить эти две функции можно оператором **bind**. 109 | 110 | ``` 111 | fun1 >>= fun2 112 | ``` 113 | 114 | Это оператор делает именно то, что делали наши маленькие функции во 2-м варианте -- с помощью case проверяет результат первой функции, и либо вызывает следующую, либо возвращает ошибку. 115 | 116 | Имея несколько таких функций, их можно соединить в цепочку оператором bind: 117 | ``` 118 | fun1 >>= fun2 >>= fun3 >>= fun4 119 | ``` 120 | 121 | Получится то же самое, что во 2-м варианте, но без явных case. 122 | 123 | Решение на Haskell выглядит так: [Main4.hs](./src_hs/Main4.hs). 124 | 125 | С Elixir нас ожидает трудность -- нет оператора bind. Но способ убрать явный case есть. Мы можем цепочку функций представить как список функций, и выполнить свертку над этим списком. 126 | ``` 127 | Pipeline.bind data, [ 128 | &fun1/1, 129 | &fun2/1, 130 | &fun3/1, 131 | &fun4/1 132 | ]). 133 | ``` 134 | 135 | Свертка реализуется тривиально: [Pipeline.ex](./src_ex/Pipeline.ex) 136 | и позволяет получить лаконичный код: [Main4.ex](./src_ex/Main4.ex). 137 | 138 | Отдельно посмотрим на валидацию списка книг. Прогнав книги через **BookShop.get_book/2** мы получим: 139 | ``` 140 | [{:ok, Book1}, {:ok, Book2}, {:ok, Book3}] 141 | ``` 142 | 143 | Но нам нужно другое. Нам нужно: 144 | ``` 145 | {:ok, [Book1, Book2, Book3]} 146 | ``` 147 | 148 | Первое во второе легко превратить с помощью **sequence**. Это стандартная функция в Haskell, и ее легко реализовать в Elixir. 149 | 150 | Есть еще одна проблема в Elixir, которой нет в Haskell. Если предыдущие варианты хорошо контролировались dialyzer, то 4-й вариант уже нет. Dialyzer ничего не может сказать о том, подходят ли функции в списке друг к другу. Поэтому лучше всего, чтобы все функции были одинаковые по сигнатуре: 151 | ``` 152 | @spec fun(my_state) -> {:ok, my_state} | {:error, some_error}. 153 | ``` 154 | Что, собственно, и сделано в 4-м варианте (кроме последней функции). 155 | 156 | 4-й вариант лаконичнее 2-го варианта. Но выигрыш не такой большой, потому что задача для него не самая подходящая. Функции **BookShop** не ложатся в pipeline непосредственно, их нужно оборачивать. Другое дело, если бы BookShop специально писался под использование в pipeline, тогда выигрыш был бы больше. 157 | 158 | На самом деле, эту задачу я составлял для 5-го варианта :) 159 | 160 | 161 | # Вариант 5. do-нотация для Elixir. 162 | 163 | Давайте посмотрим, как это выглядит в Haskell: [Main5.hs](./src_hs/Main5.hs) 164 | 165 | А выглядит это очень похоже на вариант 3 с исключениями. Только здесь нет исключений :) 166 | 167 | Мы видим два способа получить результат выполнения функции. Стрелка влево (**<-**) извлекает результат из Either, знак присваивания (**=**) извлекает обычное значение. Полученными значениями можно пользоваться ниже. Если какая-то функция вернет ошибку, то выполнение блока **do** прерывается, и ошибка возвращается как результат. 168 | 169 | Для Haskell это самый простой и лаконичный вариант. А что с Elixir? У нас есть специальная форма [with](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1), которая работает похожим образом. Мы можем точно так же извлекать значения стрелкой влево, или пользоваться обычными значениями. И точно также вычисления прекращаются, если шаблон слева от стрелки не совпал. 170 | 171 | Получается такой код: [Main5.ex](./src_ex/Main5.ex). Здесь почти все хорошо, только аргументы функций обязательно нужно окружать скобками. Увы, любимый мною безскобочный стиль в Elixir работает далеко не везде :( 172 | 173 | Можно попробовать пойти дальше, и взять настоящие монады, а не их упрощенные имитации. Для Elixir есть библиотека [Monad](https://hexdocs.pm/monad/Monad.html), и в ней монада [Error](https://hexdocs.pm/monad/Monad.Error.html), которая подходит для нашего случая. 174 | 175 | Концептуально это все тот же тип данных **{:ok, success_result} | {:error, error_result}**, но обернутый в более сложную сущность. Для нас сейчас важно, что эта сущность поддерживает do-нотацию. 176 | 177 | И получается такой код: [Main6.ex](./src_ex/Main6.ex). Разница с Main5 не большая. Синтаксис ближе к Haskell, и можно вызывать функции без скобок. 178 | 179 | Интересно, что в разных языках эта монада называется по-разному: в Haskell -- Either, в OCaml -- Result, в Elixir -- Error. Название Result мне кажется самым подходящим. 180 | 181 | 182 | # Вариант 5. do-нотация для Erlang. 183 | 184 | С Erlang ситуация сложнее, встроенных средств языка нет. 185 | Но можно попробовать придумать некий DSL, реализующий ту же идею: [main_5.erl](./src_erl/main_5.erl). 186 | 187 | Получилось заметно сложнее, и вряд ли понятно без дополнительный пояснений. Мы опять видим список. Здесь элементы списка не просто функции, а кортежи из трех элементов, где посередине находится функция, слева атом, а справа либо атом, либо что-то другое. Можно догадаться, что справа -- аргументы функции, а слева ее результат. 188 | 189 | Где-то в недрах реализации прячется контекст -- обыкновенная **map**. Атомы слева и справа -- это ключи, по которым читаются и записываются значения в контекст. Аргументом функции может быть либо конкретное значение, либо атом, и тогда значение берется из контекста. Результат функции по заданному ключу сохраняется в контексте. Последний сохраненный результат -- это результат всего блока **do**. Если какая-то функция вернет ошибку, то выполнение всего блока прекращается, и возвращается ошибка. 190 | 191 | DSL на самом деле простой, и его реализация тоже простая: [wannabe_haskell.erl](./src_erl/wannabe_haskell.erl). 192 | 193 | Для эксперимента это неплохо. При желании можно как-то развивать такой DSL, но брать в реальные проекты вряд ли стоит. 194 | 195 | С DSL понятно, но нет ли для Erlang библиотек с монадами? Есть, конечно. Для нашего случая подойдет библиотека [erlando](https://github.com/rabbitmq/erlando) от создателей rabbitmq. У Erlang, конечно, не такие возможности для метапрограммирования, как у Elixir, но авторам удалось сделать неплохую штуку. Они взяли синтаксис lists comprehension и превратили его в do-нотацию. Получилось неплохо. 196 | 197 | C монадой **error_m** и **{parse_transform, do}** получается такой вариант: [main_6.erl](./src_erl/main_6.erl). 198 | 199 | 200 | # В чем принципиальна разница между pipeline и do-notation? 201 | 202 | pipeline лучше всего подходит там, где нужно просто передавать выход одной функции на вход другой. Но у нас есть промежуточные результаты, которые нужно где-то сохранить, чтобы использовать позже. Из-за этого появляется некое состояние. 203 | 204 | Для pipeline это состояние приходиться прокидывать через все функции. Значит, функции должны знать про состояние и уметь с ним работать. Таким образом, не любую функцию можно положить в pipeline. Функции BookShop напрямую использовать нельзя, приходиться делать для них обертки, поддерживающие состояние. 205 | 206 | В случае с do-нотацией состояние существует отдельно. Можно использовать любые функции, и мы напрямую используем BookShop, без всяких оберток. 207 | 208 | 209 | # Выводы 210 | 211 | 1-й вариант -- это очевидный пример того, как делать не надо. 212 | 213 | Остальные варианты вполне годятся для использования в реальных проектах. 214 | 215 | 5-й вариант на Elixir получился лучше, на Erlang хуже. На Elixir лучше просто взять **with**, т.к. монады добавят мало чего полезного в рамках данной задачи. Для Erlang лучше взять **erlando**, чем кастомный DSL. 216 | 217 | Напоследок порекомендую книгу [Domain Modeling Made Functional. Scott Wlaschin](https://pragprog.com/book/swdddf/domain-modeling-made-functional). Прекрасное введение в функциональное программирование. 218 | --------------------------------------------------------------------------------