関数型言語の「ナゼ?」

elixir基本編

masayuki.tsuchihashi@exwzd.com

2019-09-06

  • なんでreturnないの?
  • 変数に再代入できないって、もはや定数じゃん
  • ループと言えばfor、じゃなくてEnum.map?
  • withの構文が意味不明

全部解決します

なんでreturnないの?

副作用のない関数を書くために戻り値が必須だからです。常にreturnされる事が前提なのでわざわざ書く必要もないのです。

副作用のない関数?

関数の外の世界に一切干渉しない関数です。あるのは「引数」と「戻り値」だけ。

次ページから詳しく説明します。

Without Side Effect

Has Side Effect

関数を呼ぶと外界に影響を及ぼします。

ネットワーク、DB、ファイルI/O、標準入出力、

グローバル変数への変更なども含みます。

接続エラーやDisk fullの可能性もあります。

同じ引数で呼べば必ず同じ値を返します。

これを冪等性といいます。

なのでメモ化(答えのキャッシュ)をしてもOK。

# f(x)=2x+1
def f(x: number) do
  x*2+1
end

戻り値の無い関数

副作用無しなら完璧?

OOM (Out Of Memory) Errorはあり得ます。

引数の異常を検知して例外を投げる時のように、意図的に関数コールスタックから脱出する事もあります。関数型ではあまり好まれません。

C言語やJavaで戻り値がvoidの関数を書いた時、それは暗黙に外部になにか影響を与えていることを示します。もし外部に影響を与えず、かつ戻り値もないのだったら、その関数を呼ぶ意味はないのですから。

関数コールスタックからの脱出

関数呼び出しはFILOのスタックに積まれていきます。

def a(x) do
  b(x)
  |> c
end

IO.puts(a(1))

a(x)

b(x)

c(x)

(本題とは関係なし)

c(x)で例外を生成すると、現在のスタック情報を例外に伝えて、一気に例外キャッチ元までスキップします。

returnは常に必要

毎回書きたくない

∴ 省略する

まとめ: return

→その通り。

「あんなの飾りです。偉い人にはそれがわからんのです。」

変数に再代入できないって、それ定数じゃん

一度定義した後変化しないから、IO.putsデバッグ(通称printfデバッグ)でいちいち表示して確認する必要なし。

 

別モジュールや別スレッドから変更される心配が無い

→デバッグの時に調査対象のスコープを絞り込みやすい。

副作用の無い関数に、変数は必要なし

(名前の事までは知らん)

まとめ: 再代入禁止

forはありますが上級者向けなので後に出てくるパターンマッチをやってからの方がいいです。

ループと言えばfor、

じゃなくてEnum.map?

C言語やJavaのif文やfor文は値を返さず、副作用する前提でした。

elixirでEnumerableなリストを走査するには、mapが使いやすいです。

map関数とは

左のグループから値を一つ選ぶと、対応する右のグループの値が定まります。

1

2

3

3

5

7

f(x) = 2x+1

1:1になるものを写像とか射とか言いますが本題から外れるので省略します。

map関数とは

[1,2,3]というリストに対して関数f(x)を適用

して[5,7,9]というリストを得ます。

Enum.map(1..3, fn x -> 2x+1 end)

別の型になってもいいので、こうすると3以上が数えられない子供になります。

  def f(x) do
   case x do
     1 -> "いち"
     2 -> "に"
     _ -> "たくさん"
     end
  end
f = fn x -> 2x+1 end
Enum.map(1..3, f)
1..3
|> Enum.map(fn x -> 2x+1)

別の型でもOK?

そう、Dictionary(連想配列)もMapですね。

keyに対してvalueが複数あったり(GETパラメータとか)、valueの型がバラバラの事もありますが、今はその辺の事は置いておきます。

apple

orange

kiwi

3

6

4

<key>

<value>

まとめ: map

元のEnumerableなkeyリストに対して、対応するEnumerableなvalueリストを作成しているだけ。引数1個で戻り値1個を返す副作用のない関数にピッタリです。

forが推奨されないという事ではなく、単純ループはEnum.map/2で、というだけです。for式では複数の入力ソースに対して並行に処理したい場合などに簡潔に書けます。

iex> for i <- [:a, :b, :c], j <- [1, 2], do:  {i, j}
[a: 1, a: 2, b: 1, b: 2, c: 1, c: 2]

withの構文が意味不明

with { :ok, result } <- get_result(x) do
  Maybe.new(result)
else 
  { :not_found, _ } -> Nothing 
  { :error, message} -> mix.raise(message)
end

なんで最初だけ←で、後は→なの?caseは全部右だから「このうちのどれかにひっかかるのかな」という想定ができるけど、withは想定が難しい

case get_result(x) do
  { :ok, result } -> Maybe.new(result) 
  { :not_found, _ } -> Nothing 
  { :error, message} -> mix.raise(message)
end

caseがネストするケース

    case f1.(1) do
      {:ok, v1} ->
        case f2.(v1) do
          {:ok, v2} ->
            case f3.(v2) do
              {:ok, v3} -> IO.puts("SUCCESS: #{v3}")
              {:error, msg} -> IO.puts("ERROR: #{msg}")
            end
          {:error, msg} -> IO.puts("ERROR: #{msg}")
        end
      {:error, msg} -> IO.puts("ERROR: #{msg}")
    end

(※ダジャレではない)

失敗する可能性のある関数

f1(),f2(),f3()があるとき:

成功した時だけ次のcaseを評価したいので

ネストしてしまう

withで書くと

    with {:ok, v1} <- f1.(1),
         {:ok, v2} <- f2.(v1),
         {:ok, v3} <- f3.(v2) do
      {:ok, v3} -> IO.puts("SUCCESS: #{v3}")
    else
      {:error, msg} -> IO.puts("ERROR: #{msg}")
    end

ほぼ同じ意味のコード

スッキリ&ネスト1階層

  • <- : バインドする時
  • -> : パターンマッチ

エラー処理が異なる場合は個別にハンドリングできない

なるほど、ガッテンガッテン

ちょっと待って

これどっかで見た

Eitherでは?

上級編に続く(かも)

まとめ:with

エラーの可能性のある複数の処理を、正常時のみ継続して実行したい場合に便利。