Skip to content

Instantly share code, notes, and snippets.

@lagenorhynque
Last active November 11, 2022 06:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lagenorhynque/25e659708303837ef71430f3bf2cd536 to your computer and use it in GitHub Desktop.
Save lagenorhynque/25e659708303837ef71430f3bf2cd536 to your computer and use it in GitHub Desktop.
メタプログラミング入門

メタプログラミング入門


(defprofile lagénorhynque
  :id           @lagenorhynque
  :reading      "/laʒenɔʁɛ̃k/"
  :aliases      ["カマイルカ🐬"]

  :languages    [Clojure Haskell English français]

  :interests    [programming language-learning law mathematics]

  :commits      ["github.com/lagenorhynque/duct.module.pedestal"
                 "github.com/lagenorhynque/duct.module.cambium"]
  :contributes  ["github.com/japan-clojurians/clojure-site-ja"])

twitter icon


  1. メタプログラミングという「魔術」

  2. 「魔術」を使うべきか

  3. 主な「魔術」の紹介

    • 🔥 未定/不存在のものを呼び出す
    • 🌿 既存の定義を変更する
    • 💧 構文を操作する

メタプログラミングという

「魔術」


メタプログラミング(metaprogramming)の定義(例)

『メタプログラミングRuby 第2版』では

メタプログラミングとは、

コードを記述するコードを記述することである。

※ ただし、(メタプログラミング的でない)プログラミングとメタプログラミングの境界はそもそも曖昧


メタプログラミングの分類(例)

  • 実行時メタプログラミング(or 動的メタプログラミング)
  • コンパイル時メタプログラミング(or 静的メタプログラミング)

※ ただし、複数の処理フェーズに関わるメタプログラミング手法もある


「魔術」を使うべきか


典型的なメリット

  • 利用側のコードの読み書きが簡単になる
  • 困難/不可能を可能にする
    • 動的な定義の生成/変更
    • 評価の制御
    • etc.
  • DSL (domain-specific language)の表現力が高まる
  • パフォーマンスが改善する
    • コンパイル時に処理を済ませられる
    • etc.

典型的なデメリット

  • 定義側のコードの読み書きが難しくなる
  • 機能制限が生じる
    • 静的解析との相性が悪くなる
    • ファーストクラスの構成要素と組み合わせづらくなる
    • etc.
  • パフォーマンスが劣化する
    • 実行時のオーバーヘッドが増える
    • コンパイル時間が長くなる
    • etc.

e.g. マクロを書く際の指針

Macro Club has two rules, plus one exception. The first rule of Macro Club is Don’t Write Macros. (...) The second rule of Macro Club is Write Macros If That Is the Only Way to Encapsulate a Pattern. (...) The exception to the rule is that you can write any macro that makes life easier for your callers when compared with an equivalent function.

Programming Clojure, Third Edition


主な「魔術」の紹介


🔥 未定/不存在のものを呼び出す


Class, Methodクラスでのリフレクション (Java)

jshell> class Greeting {
   ...>     private String name;
   ...>     Greeting(String name) {
   ...>         this.name = name;
   ...>     }
   ...>     String hello() {
   ...>         return "Hello, " + name + "!";
   ...>     }
   ...>     private String bonjour() {
   ...>         return "Bonjour, " + name + " !";
   ...>     }
   ...> }
|  created class Greeting

jshell> var g = new Greeting("lagénorhynque")
g ==> Greeting@377dca04

jshell> g.hello()
$3 ==> "Hello, lagénorhynque!"

jshell> g.bonjour()
|  Error:
|  bonjour() has private access in Greeting
|  g.bonjour()
|  ^-------^

jshell> Greeting.class.getDeclaredMethods()
$4 ==> Method[2] { java.lang.String Greeting.hello(), private ja
va.lang.String Greeting.bonjour() }

jshell> var mHello = Greeting.class.getDeclaredMethod("hello")
mHello ==> java.lang.String Greeting.hello()

jshell> mHello.invoke(g)
$6 ==> "Hello, lagénorhynque!"

jshell> var mBonjour = Greeting.class.getDeclaredMethod("bonjour
")
mBonjour ==> private java.lang.String Greeting.bonjour()

jshell> mBonjour.invoke(g)
|  Exception java.lang.IllegalAccessException: class REPL.$JShell$23 cannot access a member of class REPL.$JShell$11$Greeting with modifiers "private"
|        at Reflection.newIllegalAccessException (Reflection.java:385)
|        at AccessibleObject.checkAccess (AccessibleObject.java:693)
|        at Method.invoke (Method.java:556)
|        at (#10:1)

jshell> mBonjour.setAccessible(true)

jshell> mBonjour.invoke(g)
$12 ==> "Bonjour, lagénorhynque !"

  • 他言語での例

method_missingメソッド(Ruby)

irb(main):001:1* class Greeting
irb(main):002:2*   def initialize(name)
irb(main):003:2*     @name = name
irb(main):004:1*   end
irb(main):005:2*   def method_missing(m, *args)
irb(main):006:2*     "#{m.capitalize}, #{@name}!"
irb(main):007:1*   end
irb(main):008:1* end
=> :method_missing
irb(main):009:0> g = Greeting.new("lagénorhynque")
=> #<Greeting:0x00007fcb472e0198 @name="lagénorhynque">
irb(main):010:0> g.hello
=> "Hello, lagénorhynque!"
irb(main):011:0> g.bonjour
=> "Bonjour, lagénorhynque!"


🌿 既存の定義を変更する


open class (Ruby)

irb(main):001:1* class String
irb(main):002:2*   def hello
irb(main):003:2*     "Hello, #{self}!"
irb(main):004:1*   end
irb(main):005:1* end
=> :hello
irb(main):006:0> "lagénorhynque".hello
=> "Hello, lagénorhynque!"

irb(main):001:1* module Greeting
irb(main):002:2*   refine String do
irb(main):003:3*     def hello
irb(main):004:3*       "Hello, #{self}!"
irb(main):005:2*     end
irb(main):006:1*   end
irb(main):007:0> end
=> #<refinement:String@Greeting>

irb(main):008:1* module M
irb(main):009:1*   using Greeting
irb(main):010:2*   class << self
irb(main):011:3*     def f(name)
irb(main):012:3*       name.hello
irb(main):013:2*     end
irb(main):014:1*   end
irb(main):015:0> end
=> :f

irb(main):016:0> M.f("lagénorhynque")
=> "Hello, lagénorhynque!"
irb(main):017:0> "lagénorhynque".hello
Traceback (most recent call last):
        4: from /Users/lagenorhynque/.rbenv/versions/3.0.0/bin/irb:23:in `<main>'
        3: from /Users/lagenorhynque/.rbenv/versions/3.0.0/bin/irb:23:in `load'
        2: from /Users/lagenorhynque/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
        1: from (irb):17:in `<main>'
NoMethodError (undefined method `hello' for "lagénorhynque":String)


irb(main):001:0> name = "lagénorhynque"
=> "lagénorhynque"
irb(main):002:1* def name.hello
irb(main):003:1*   "Hello, #{self}!"
irb(main):004:1* end
=> :hello
irb(main):005:0> name.hello
=> "Hello, lagénorhynque!"
irb(main):006:0> "lagénorhynque".hello
Traceback (most recent call last):
        4: from /Users/lagenorhynque/.rbenv/versions/3.0.0/bin/irb:23:in `<main>'
        3: from /Users/lagenorhynque/.rbenv/versions/3.0.0/bin/irb:23:in `load'
        2: from /Users/lagenorhynque/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
        1: from (irb):6:in `<main>'
NoMethodError (undefined method `hello' for "lagénorhynque":String)


💧 構文を操作する


マクロ(Clojure)

user=> (defmacro unless [test & body]
         `(when (not ~test)
            ~@body))
#'user/unless

user=> (unless (= 1 2)
         (println "not equal"))
not equal
nil
user=> (unless (= 1 1)
         (println "not equal"))
nil

user=> (macroexpand-1
        '(unless (= 1 2)
           (println "not equal")))
(clojure.core/when (clojure.core/not (= 1 2)) (println "not equa
l"))

マクロ(Elixir)

iex(1)> defmodule M do
...(1)>   defmacro unless(test, do: body) do
...(1)>     quote do
...(1)>       if !unquote(test) do
...(1)>         unquote(body)
...(1)>       end
...(1)>     end
...(1)>   end
...(1)> end
{:module, M,
 <<70, 79, 82, 49, 0, 0, 5, 120, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 148,
   0, 0, 0, 17, 8, 69, 108, 105, 120, 105, 114, 46, 77, 8, 95, 95, 105, 110,
   102, 111, 95, 95, 10, 97, 116, 116, 114, ...>>, {:unless, 2}}

iex(2)> require M
M
iex(3)> M.unless 1 == 2 do
...(3)>   IO.puts "not equal"
...(3)> end
not equal
:ok
iex(4)> M.unless 1 == 1 do
...(4)>   IO.puts "not equal"
...(4)> end
nil

iex(5)> quote do
...(5)>   M.unless 1 == 1 do
...(5)>     IO.puts "not equal"
...(5)>   end
...(5)> end |> Macro.expand_once(__ENV__) |>
...(5)>   Macro.to_string |> IO.puts
if(!(1 == 1)) do
  IO.puts("not equal")
end
:ok


  • メタプログラミングを「魔術」のままにせず

    • 仕組みに対する理解を深めよう

    • 適切に恐れ、適切に活用しよう

lisp alien


Further Reading

#!/usr/bin/env bash
# npm install -g reveal-md
reveal-md introduction-to-metaprogramming.md --theme night --highlight-theme monokai-sublime -w $@
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment