elixirだ 第2回

54
第2回 - メタプロだ - GMO Pepabo, Inc. Joe Honzawa 2015/5/20 Elixir勉強会 Elixirだ

Upload: joenoh

Post on 21-Jul-2015

209 views

Category:

Technology


0 download

TRANSCRIPT

第2回 - メタプロだ -

GMO Pepabo, Inc. Joe Honzawa

2015/5/20 Elixir勉強会

Elixirだ

前回やったこと> Elixirのインストール > 型 > 制御構文case, if, cond > パターンマッチ > モジュール、関数、ディレクティブ > MixとHex > Mixプロジェクトの作成とテスト

今回の内容> メタプログラミング > ElixirのAST > マクロ > はじめてのマクロ > assert > コード生成

分かりづらい内容なので、 同じことを繰り返し言ったりします

ElixirのAST

ASTとは> Abstract Syntax Tree > 抽象構文木 > コードの内部表現 > コードの意味を表す木構造

ElixirのAST> Elixirのデータ型で表現される > サイズ3のタプル > {関数名, メタデータ, 引数たち} > の入れ子

> Elixirの関数で扱える ← これ重要

ElixirのASTの例is_atom(:ok)

↓ { :is_atom, [context: Elixir, import: Kernel], [:ok] }

例外> (quote do: me) == me > atoms > numbers > lists > strings > tuples with two elements

quoteなしでASTとして使えるという意味でもある

ASTを観察しよう$ iex

iex(1)> quote do ...(1)> String.length(“hey”) ...(1)> end

iex(2)> {name, _, args} = v(1)

iex(3)> Code.eval_quoted v(1)

quoteとunquote> quote > 与えられたコードをASTに変換

> unquote > 与えられたASTをコードに埋め込む

unquoteしようiex(1)> ast = quote do: String

iex(2)> quote do ...(2)> (unquote ast).length “hey” ...(2)> end

iex(3)> {name, _, args} = v(2)

iex(4)> Code.eval_quoted v(2)

おさらい> quote > 与えられたコードをASTに変換

> unquote > 与えられたASTをコードに埋め込む

マクロ

マクロ> ASTを受けてASTを返す > defmacroで定義する >もちろんモジュールの内側で > defmacropならプライベート

defmacro ukenagasu(ast) do ast end

ukenagasu IO.puts(“hey”)

> 入力をそのまま返すマクロ

> astはIO.puts(“hey”)のAST

ifマクロif now == :morning do “good morning” else “hi” end

ifマクロif(now == :morning do “good morning” else “hi” end)

ifマクロif(now == :morning, do: “good morning”, else: “hi” )

ifマクロif(now == :morning, [do: “good morning”, else: “hi”] )

ifの定義defmacro if(condition, clauses) do do_cl = K.get(clauses, :do) else_cl = K.get(clauses, :else) quote do case unquote(condition) do x when x in [false, nil] -> unquote(else_cl) _ -> unquote(do_cl) end end end ※ 一部略。KはKeywordのalias

defmacro if(condition, clauses) do do_cl = K.get(clauses, :do) else_cl = K.get(clauses, :else) quote do case unquote(condition) do x when x in [false, nil] -> unquote(else_cl) _ -> unquote(do_cl) end end end

ifの定義

※ 一部略。KはKeywordのalias

clausesはASTでは?

defmacro if(condition, clauses) do do_cl = K.get(clauses, :do) else_cl = K.get(clauses, :else) quote do case unquote(condition) do x when x in [false, nil] -> unquote(else_cl) _ -> unquote(do_cl) end end end

ifの定義

※ 一部略。KはKeywordのalias

リストと2要素タプルは ASTになっても形を保つので Keywordモジュールでイジれる

ifの定義defmacro if(condition, clauses) do do_cl = K.get(clauses, :do) else_cl = K.get(clauses, :else) quote do case unquote(condition) do x when x in [false, nil] -> unquote(else_cl) _ -> unquote(do_cl) end end end ※ 一部略。KはKeywordのalias

ifマクロ展開後if(now == :morning, do: “good morning”, else: “hi”)

↓ case now == :morning do x when x in [false, nil] -> “hi” _ -> “good morning” end

アレもコレもマクロ> is_nil > def > defmodule > |> > .. (Rangeのあれ)

>他にもいっぱい

パイプ |>[1, 2, 3] |> Enum.map(fun)

Enum.map([1, 2, 3], fun)

第1引数として差し込む

はじめてのマクロ

簡単なやつね

$ mix new makuro $ cd ./makuro

つくっちゃってー>与えられたコードを2回実行するマクロ

>ヒントdefmacro twice(do: content) do quote do unquote(content) unquote(content) end end

テストするよdefmodule MakuroTest do use ExUnit.Case import ExUnit.CaptureIO require Makuro

test “twice” do output = capture_io fn -> Makuro.twice do: IO.puts “age” end assert output == “age\nage\n” end end

require?> Elixirのコンパイル > コードをASTにパース > 展開フェイズ > マクロの展開を繰り返す > バイトコードへ変換

require?> “マクロを展開する” > マクロを呼び、ASTを得ること > マクロは展開フェイズで呼ばれる > コンパイルされてないと呼べない > 先にコンパイルしておいて欲しいモジュールを示すためにrequire

useもマクロuse Makuro, options

 ↓

require Makuro Makuro.__using__(options)

例えばこれが1行でdefmodule Makuro do defmacro __using__(_) do quote do import Makuro import Makuro.Yabai import Makuro.Kakkoii import Makuro.TotemoYoi end end end

use Makuro

assert

前回のラスト

ワンダーだった点> ==で比較したことがバレていた

> まさか、quote??????

assert(a == b) → assert( true )ではない?

試してみようiex(1)> quote do: 1 == 2 iex(2)> {ope, _, [lhs, rhs]} = v(1)

iex(3)> quote do ...(3)> String.length(“hi”) == 2 ...(3)> end iex(4)> {ope, _, [lhs, rhs]} = v(3)

 比較演算子、右辺、左辺が取得できた

オレオレassertdefmacro tadashii(expr) do {ope, _, [lhs, rhs]} = expr quote do left = Macro.to_string(unquote lhs) right = Macro.to_string(unquote rhs)

IO.puts “ひだり: “ <> left IO.puts “みぎ:  “ <> right if unquote(expr) do IO.puts “こいつら #{unquote ope}” else IO.puts “こいつら #{unquote ope} じゃない” end end end

オレオレassertiex(1)> require Makuro

iex(2)> Makuro.tadashii 1 == 1 ひだり: 1 みぎ: 1 こいつら ==

iex(3)> Makuro.tadashii 1 == 2 ひだり: 1 みぎ: 2 こいつら == じゃない

マクロは強い> 標準ライブラリにもマクロがいっぱい

> 発想次第でいろいろ出来る > DSLの構築とか

マクロは難しい> 悪魔的AST変形 > 狙い通りに展開されているのか

> どうする > 関数でできることは関数でやる > テストする

ちょっとオマケ

展開後のコードを見る

test/test_helper.exsExUnit.start

defmodule TestHelper do defmacro puts_expanded(macro) do macro |> Macro.expand(__CALLER__) |> Macro.to_string |> IO.puts end end

test/makuro_test.exs

defmodule MakuroTest do import TestHelper

test “see expanded” do (String.length(“hey”) == 3) |> Makuro.tadashii |> puts_expanded end end

コード生成

変なことしてみる

defmodule Makuro do

IO.puts “just inside defmodule”

end

$ mix compile

どういうこと?> コンパイル時にElixirコードが実行された > def等がマクロということを考えれば当然

> IOモジュールはコンパイル済みだった > brew installしたとき

> なのでMakuroコンパイル時にputsできた

> 他にもEnumとか使えるのでは?

似た関数をループで定義defmodule Makuro do [ {“mon”, “月”}, {“tue”, “火”}, … ] |> Enum.map fn {en, ja} -> def in_japanese(unquote en) do unquote ja end end end

もっと> ファイルの内容に基づいて関数を定義 > StringモジュールとUnicode > https://hex.pm/packages/colorful

やってみよ>名簿ファイルを読んで、Person構造体を返す関数群を定義しよう

> Person.alex/0 > %Person{name: "alex", age: 20}

alex 20 jack 18 mary 22

priv/people.txt

ヒント> File.read/1 > String.split/2 > String.to_integer/1

> fn_name = String.to_atom(name) > def unquote(fn_name)() do

> (File.stream!)

今回やったこと> メタプログラミング > ElixirのAST > マクロ > ExUnitのassert > コード生成