Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

momenti controls

articles/momenti-controls.livemd

momenti controls

setup

Momenti.Control 에 있는 모듈들 & Witchcraft 기능들을 학습해보기 위한 노트북

momenti-elixir apps/momenti_core 앱을 다음과 같이 실행합니다.

$ iex --name momenti@cmmn.chitacan.io --cookie hello -S mix

LiveBook 에서는 Runtime Settings ➡️ Attached Node 를 선택합니다.

use Momenti.Control

Basic Types

Right.new()
Left.new()
Error.new(:raw, :reason, :detail)
Monad.left_error(:error)

chain

zeroTrue = fn val ->
  if val == 0 do
    Right.new(:ok)
  else
    Left.new(:err)
  end
end
chain do
  # :ok
  :ok <- zeroTrue.(0)
  # :err
  zeroTrue.(1)
  # skipped
  zeroTrue.(0)
end

Monad.either

random = fn ->
  case Enum.random(0..3) do
    0 -> Right.new(0)
    1 -> Left.new(1)
    2 -> Error.new(:raw, :reason, :detail)
    3 -> 3
  end
end
random.() |> Monad.either()

monad

monad [] do
  return(1)
end
monad [] do
  :ok <- zeroTrue.(0)
  # :err
  zeroTrue.(1)
  # skipped
  zeroTrue.(0)
end

Algae.Reader

https://hexdocs.pm/algae/Algae.Reader.html

correct =
  monad %Reader{} do
    count <- Reader.ask(&amp;Map.get(&amp;1, :count))
    bindings <- Reader.ask()
    return(count == map_size(bindings))
  end
correct |> Reader.run(%{count: 3})
correct |> Reader.run(%{count: 3, a: 1, b: 2})

defr

https://github.com/trevorite/defr

defmodule Target do
  use Defr

  import Enum, only: [at: 2]

  defr top(list) do
    list |> List.flatten() |> inject() |> middle() |> run()
  end

  defr middle(list) do
    list |> bottom() |> inject() |> run()
  end

  defrp bottom(list) do
    %{pos: pos} <- ask()
    list |> at(pos) |> inject()
  end
end
Target.middle([10, 2, 3]) |> Reader.run(%{pos: 2})

middle/1 에서 bottom/1inject 되었기 때문에, Reader.run 에서 다른 함수(replaced)로 대체할 수 있다.

pos 의 값은 앞선 코드와 같이 2 이기 때문에, 결과는 3 ([10,2,3] |> Enum.at(2)) 이 나와야 한다.

하지만, bottom/1replaced 로 대체되어 실행되기 때문에 결과가 다르다.

replaced = fn list ->
  monad %Reader{} do
    return(List.first(list))
  end
end

Target.middle([10, 2, 3])
|> Reader.run(%{
  :pos => 2,
  # middle 에서 사용되는 bottom 을 변경할 수 있다!!
  &amp;Target.bottom/1 => replaced
})
env = %{}
[1, 2, 3] |> Map.get(env, &amp;Enum.at/2, &amp;List.first/2).(1)

~> operator

alias for Functor.lift/2

%Right{} 로 감싸진 값에 함수를 적용할 수 있다.

Right.new(10) ~> fn x -> x + 10 end ~> fn x -> x + 20 end
Witchcraft.Functor.lift(Right.new(10), fn x -> x * 2 end)
[1, 2, 3] ~> fn x -> x + 10 end ~> fn x -> x + 20 end
Left.new(:error) ~> fn x -> x + 20 end ~> fn x -> x + 10 end
Right.new([10, 20, 30]) ~> (&amp;Enum.map(&amp;1, fn x -> x + 10 end))

operator

alias for chain/2 (looks like flatmap)

%Right{} 로 감싸진 값에 %Right{} 를 리턴하는 함수를 적용할 수 있다.

Right.new(10) >>> fn x -> Right.new(x + 90) end
[10, 20, 30] >>> fn x -> [x * 10] end
Right.new(10) >>> fn _x -> Left.new(:error) end

async_traverse

async_traverseasync_map 이후에 traverse 를 실행하는 함수다.

def async_map(enumerable, func, max_concurrency \\ nil) do
  max_concurrency = max_concurrency || :erlang.system_info(:schedulers) * 16
  chunk_size = div(Enum.count(enumerable) - 1, max_concurrency) + 1

  enumerable
  |> Enum.chunk_every(chunk_size)
  |> Enum.map(fn chunks ->
    Task.async(fn ->
      chunks |> Enum.map(func)
    end)
  end)
  |> Task.await_many(:infinity)
  |> Enum.flat_map(&amp; &amp;1)
end

enumerable 을 chunk 단위로 나누고, 별도의 Task 프로세스에서 effect 함수를 실행하는 구조

2번째 인자에는 Algae.Either 를 리턴해도 된다.

Monad.async_traverse([1, 2, 3], fn x -> Right.new(x * 100) end)
Monad.async_traverse([1, 2, 3], fn x ->
  if x == 2, do: Left.new(:error), else: Right.new(x * 100)
end)

async_traverse 로 생성된 Task 를 트레이스 할 수 있을까?