2021-11-12 Elixir Team Weekly Recap
fold / unfold
fold 와 reduce 는 완전히 같은 동작이다. 둘을 굳이 구분해서 사용하고 싶다면, unfold 가 가능한 상황에서는 fold 를 unfold 가 불가능한 상황에서는 reduce 를 사용한다. 그렇다면 unfold 가 가능한 fold 는 무엇이 있을까?
파일의 목록을 하나의 파일로 만드는 동작 (zip, tar, giva) 은 unfold 가 가능한 동작이라 할 수 있다. 다시말해 역함수가 있는 reduce 동작은 fold / unfold 로 구분하면 좋다.
Elixir 에서는 Enum 에는 reduce, List 에는 fold 란 이름으로 구분되어 있다.
List.foldl([1, 2, 3], 0, fn x, acc -> x + acc end)
unfold 는 Stream 모듈에 구현되어 있다.
Stream.unfold(10, fn
0 -> nil
n -> {n, n - 1}
end)
|> Enum.to_list()
:inet.gethostname()
Decode giva with binary pattern match
web-momenti-player 의 resource_manager.ts 파일 에 구현된 parseActionBytes 의 구현을 Elixir 로 가져와 테스트 코드까지 작성
테스트 코드를 포함해도 라인수가 기존 ts 코드보다 훨씬 줄어들었는데, 여기에 사용된 트릭들을 연습해보자
defmodule Giva do
@identifier <<170, 187, 204, 221, 221, 204, 187, 170>>
@image_type 0
@giva_file "../data/bf506159-3d7b-4242-a254-a24b7b313e3c@0968E872-DBCE-43E0-BF92-B127C4D6A7EC_3_50.giva"
def read() do
@giva_file |> File.read!()
end
def identifier, do: @identifier
def decode(<>) do
bin
|> Stream.unfold(fn
@identifier <>
<> ->
{{frame_id, image_bin}, next}
<<>> ->
nil
end)
|> Enum.to_list()
end
end
<<>>
https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1
바이너리 (bitstring) 은 여러가지 type, option, size 로 매치할 수 있다.
binary-size 는 8bit 단위이다. <<>>
외부에서는 byte_size
와 같다.
size 는 1bit 단위.
identifier = Giva.identifier()
identifier_size = byte_size(identifier)
<<_::binary-size(identifier_size), frame_id::binary-size(36), rest::binary>> = Giva.read()
frame_id
하나의 바이너리 파일을 unfold 를 사용해 {frameId, image_bin}
의 목록으로 변환한다.
Giva.read() |> Giva.decode()
> And the variable can be defined in the match itself (prior to its use):
항목을 통해 decode
함수를 좀 더 간결하게 작성할 수 있다는 것을 알게되었다.
before
def decode(<>) do
bin
|> Stream.unfold(fn
@identifier <>
<> ->
<> = rest
{{frame_id, image_bin}, next}
<<>> ->
nil
end)
|> Enum.to_list()
after
def decode(<>) do
bin
|> Stream.unfold(fn
@identifier <>
<> ->
{{frame_id, image_bin}, next}
<<>> ->
nil
end)
|> Enum.to_list()
<<>>
안에 string 이 들어 있으면 자동으로 binary 로 expand 해준다.
<> =
<<5, "Frank the Walrus">>
{name, species}
<> = <<10, 20, 30>>
rest
traverse
traverse 는 기본적으로 sequence + map 의 합성이다. 따라서 traverse 를 이해하기 위해서는 sequence 를 정확하게 이해하고 있어야 한다.
먼저 witchcraft 를 livebook 에 설치해보자. (단순히 Mix.install
을 호출해서는 설치가 되지 않는다.)
Mix.install(
[
{:algae, "~> 1.3", override: true, app: false},
{:quark, "~> 2.3", override: true, app: false},
{:type_class, "~> 1.2", override: true, app: false},
{:exceptional, "~> 2.1", override: true, app: false},
{:operator, "~> 0.2", override: true, app: false},
{:witchcraft, git: "https://github.com/jechol/witchcraft.git", override: true, app: false}
],
force: true
)
use Witchcraft
alias Algae.Either.Right
alias Algae.Either.Left
sequence 는 [Promise, Promise, Promise]
를 받아서 Promise<[T]>
를 반환하는 Promise.all
과 유사하다.
sequence([[1]])
sequence([Right.new(10), Right.new(20), Right.new(30)])
sequence([Left.new(10), Right.new(20), Left.new(30)])
Either
의 동작은 예상한대로 였지만, Tuple
의 경우 왜 이렇게 동작하는지 정확하게 파악하기가 어렵다.
sequence({Right.new(10), Right.new(20), Right.new(30)})
sequence({[1, 2]})
sequence([{1}])
sequence([{1}, {5}])
traverse([1, 2, 3], fn x -> Right.new(x * 10) end)
최종적으로는 여기에 사용된 async_traverse
를 이해하고 싶은데, async_traverse
는 Momenti.Control.Monad
에 자체적으로 정의된 함수임
따라서 나머지는 다른 노트북 에서 진행하겠음