読者です 読者をやめる 読者になる 読者になる

テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(mal report) STEP3の環境をAgentで実装する

STEP3では, def!というコマンドを実装する. これは, 環境を変化させる. これは副作用を意味しており, 関数型言語ではナチュラルに書けないことが多い.

Haskellでは以下のようにIORefを使って実装してある.

env_set :: Env -> MalVal -> MalVal -> IO MalVal
env_set envRef (MalSymbol key) val = do
    e <- readIORef envRef
    case e of
        EnvPair (o,m) -> writeIORef envRef $ EnvPair (o, (Map.insert key val m))
    return val

もっとも愚直に行うならば, あらゆる評価を評価値と環境のペアとして, 環境を渡していくという実装があり得るが, そういうものを全域にtrampさせるのは, あまりにも汚い.

このような時には, ErlangのAgentを使うと良いことを思い出した.

http://www.ymotongpoo.com/works/lyse-ja/ja/13_multiprocessing.html#state-your-state

Elixirでも実装されている. これを使うときれいに書けると思う.

Agent - Elixir

{:ok, pid} = Agent.start_link(fn () -> %{} end)
Agent.update(pid, fn t -> Dict.put(t, "a", 1) end)
IO.inspect(Agent.get(pid, fn t -> t end))

(追記)

こんな感じでいけるだろうと思う.

defmodule MAL.Env do
  defstruct data: %{}, outer: nil

  def do_set(env, k, v) do
    newdata = Dict.put(env.data, k, v)
    %{env | data: newdata}
  end

  # :: Env
  def do_find(env, k) do
    e = env.data[k]
    if e do
      env
    else
      env.outer.find[k]
    end
  end

  # :: value
  def do_get(env, k) do
    e = do_find(env, k)
    if e do
      e.data[k]
    else
      raise ArgumentError, message: "#{k} not found"
    end
  end

  def do_new(outer),
  do: %MAL.Env{outer: outer}

  def new(outer) do
    {:ok, pid} = Agent.start_link(fn -> do_new(outer) end)
    pid
  end

  def set(pid, k, v),
  do: Agent.update(pid, fn env -> do_set(env, k, v) end)

  # :: Env
  def find(pid, k),
  do: Agent.get(pid, fn env -> do_find(env, k) end)

  def get(pid, k),
  do: Agent.get(pid, fn env -> do_get(env, k) end)
end

# e = MAL.Env.do_new(nil)
# e = MAL.Env.do_set(e, "a", 1)
# MAL.Env.do_get(e, "a") |> IO.inspect

pid = MAL.Env.new(nil)
MAL.Env.set(pid, "a", 1)
MAL.Env.get(pid, "a") |> IO.inspect