LoginSignup
127
124

More than 5 years have passed since last update.

ラムダ式で学ぶクロージャとDSL

Last updated at Posted at 2012-12-26

はいこんにちはこんにちは! 今日はラムダ式でクロージャとDSLの解説をします。

ガチなラムダ計算の話ではまったくないのでご安心ください。函数型プログラミング言語の話でもないので怯える必要はありません。

それと、実用なDSLそのものではなく、どちらかといふと ふざけたDSL をシンプルに作ってみました! といふ例題なのでその点はご承知おきください。

謎のコピペとDSL

まづはこちらのソースコードをお読みください。

tsuyoi.rb
# -*- Coding: utf-8 -*-
scripter = ->(*args){->{puts args.join("\n")}}
title = ->(text){"『#{text}\n"}
paragraph = ->(*args){args.join("\n")+"\n"}
chara = ->(name){
  ->(text){"#{name}#{text}」"}
}

ryu = chara[:リュウ]
tak = chara[:たかし]
script = scripter[
  title[:俺より強い奴に、会いに行く],
  paragraph[
    :ピンポーン,
    ryu[:こんにちは],
    tak[:はいどなた]
  ],paragraph[
    ryu[:いまちょっといいですか], 
    tak[:これから出かけます]
  ],paragraph[
    ryu[:午後出勤ですか?],
    tak[:はい]
  ],paragraph[
    ryu[:強そうですね],
    tak[:なにがですか]
  ],paragraph[
    ryu[:態度が]
  ],paragraph[
    :リュウは、自分より強そうな奴に、会いにいったのだった。 完 
  ]
]

script[]

これは何の言語のコードかおわかりですね。 はい、みんな大好きなRubyです! 僕も愛してるよ!

それはさておいて、このコードが「まともな」Rubyに見えないことは、これを書いた僕にも認めざるを得ません。 (ここで Rubyなんかわからねえよ と感想をお持ちになった方もご安心ください。このページの末尾にJavaScript版も用意してありますので、そちらを先にお読んでも差支へありません!)

このコードを実行すると次のように出力されます。

out1.txt
『俺より強い奴に、会いに行く』

ピンポーン
リュウ「こんにちは」
たかし「はいどなた」

リュウ「いまちょっといいですか」
たかし「これから出かけます」

リュウ「午後出勤ですか?」
たかし「はい」

リュウ「強そうですね」
たかし「なにがですか」

リュウ「態度が」

リュウは、自分より強そうな奴に、会いにいったのだった。 完

このコピペ(と、僕の書いたRubyコード)の初出は 2011年11月15日のVIPミングスレの>>325 です。が、この流れでなぜこんな意味不明なコピペが貼られたのか、未だに謎です。唐突すぎて意味がまったくわかりません。

それはさておいて昼休みも半ばを過ぎた頃になってこのスレを読んだ僕は思ったのでした。ああ、これを出力してやるRubyコードを書いてレスしてやらねばならないな、と……。そしてまた思ひました。「ラムダ式の組み合せでコードを構成してやりたい」そして、「それは立派なDSLと呼べるのではないか」と。

……などと考へた結果が以上のコードになりました。

DSLとは何か

さて、以上のような出力は実は次のようなRubyコードでも得ることが可能です。

yowai.rb
# -*- Coding: utf-8 -*-
puts '『俺より強い奴に、会いに行く』'
puts 
puts 'ピンポーン'
puts 'リュウ「こんにちは」'
puts 'たかし「はいどなた」'
puts 
puts 'リュウ「いまちょっといいですか」'
puts 'たかし「これから出かけます」'
puts 
puts 'リュウ「午後出勤ですか?」'
puts 'たかし「はい」'
puts 
puts 'リュウ「強そうですね」'
puts 'たかし「なにがですか」'
puts 
puts 'リュウ「態度が」'
puts 
puts 'リュウは、自分より強そうな奴に、会いにいったのだった。 完'
puts ''

「これで良いじゃん」と思はれる方がいらっしゃるかもしれません。……ほんとにそうですか?

たとへば、せりふを 人物「せりふ」 ではなくて 【人物】せりふ の形式にしたいと思ったとき、どうしますか? 一度直接書いてしまったものを後から変更するのは、なかなかに面倒なことです。 (ふつうはエディタで一括置換することになりますよね。しかしそれでも、意図した通りに処理できないのはよくあることです)

上記のスクリプトで生成した場合なら、こんなパッチを当ててやるだけです。ひどくお手軽ですね。

tsuyoi.patch
--- tsuyoi.rb   2012-10-28 03:08:17.648000000 +0900
+++ b.rb        2012-10-28 03:08:03.516000141 +0900
@@ -2,7 +2,7 @@
 title = ->(text){"『#{text}』\n"}
 paragraph = ->(*args){args.join("\n")+"\n"}
 chara = ->(name){
-  ->(text){"#{name}「#{text}」"}
+  ->(text){"【#{name}】 #{text}"}
 }

 ryu = chara[:リュウ]

実行してみませう。

out2.txt
『俺より強い奴に、会いに行く』

ピンポーン
【リュウ】 こんにちは
【たかし】 はいどなた

【リュウ】 いまちょっといいですか
【たかし】 これから出かけます

【リュウ】 午後出勤ですか?
【たかし】 はい

【リュウ】 強そうですね
【たかし】 なにがですか

【リュウ】 態度が

リュウは、自分より強そうな奴に、会いにいったのだった。 完

と、そんな感じでデータを 抽象化 するメリットについてはおわかりいただけたことと思ひます。そしてDSLはそれを実現するためのひとつの手段です。

DSL —— ドメイン固有言語 とは、特定の目的のために特化したプログラミング言語を設計することです。

DSLにも大別すると二種類があって、作ったDSLがホスト言語(つまり、作成した言語を処理するプログラムを作成するために使用した言語)の言語仕様と関係のない独立した「外部DSL」と、ホスト言語の仕様を積極的に利用する「内部DSL」に分けることができます。

「外部DSL」「内部DSL」はそれぞれ「言語外DSL」「言語内DSL」と呼ぶこともあります。

この分類でいくと、この謎コピペを記述するために書いたDSLは「Rubyを使った内部DSL」だと言ふことができます。

tsuyoi_script.rb
ryu = chara[:リュウ] # キャラクター「リュウ」を作成
tak = chara[:たかし] # キャラクター「たかし」を作成

# ここからシナリオを記述
script = scripter[
  # タイトルを表示
  title[:俺より強い奴に、会いに行く],
  #段落ごとに分けて記述できる
  paragraph[
    # 地の文はそのまま書く
    :ピンポーン,
    # キャラクターのせりふ
    ryu[:こんにちは],
    tak[:はいどなた]
  ],paragraph[
    ryu[:いまちょっといいですか], 
    tak[:これから出かけます]
  ],paragraph[
    ryu[:午後出勤ですか?],
    tak[:はい]
  ],paragraph[
    ryu[:強そうですね],
    tak[:なにがですか]
  ],paragraph[
    ryu[:態度が]
  ],paragraph[
    :リュウは、自分より強そうな奴に、会いにいったのだった。 完 
  ]
]
# シナリオを終了

これらのDSLには、それぞれ一長一短があります。

内部DSLは飽くまで「独自言語に 見せかけた 」ホスト言語で書かれたプログラムに過ぎません。しかしその代りに、ホスト言語の機能を利用できるために、DSL作者の手間は(相対的に見て)少なくなります。また、DSL利用者はホスト言語に対応した開発環境の支援機能を利用することができます。短所としては、ホスト言語を他の言語に移植しようとするとまったく同じ言語にできないことがある、もしくはまったく同じにしようとすると他のホスト言語では 外部DSLになってしまふ ことがある、といふことがあります。

外部DSLは、その言語で利用したい機能を作者が想定し、実装してやる必要があります。そのため、開発にかかるコストは単純なものならばさほどではありませんが、多機能なものでは機能の多さに比例して非常に複雑になり、困難なものになっていきます。これはデメリットに見えるかもしれませんが、DSL作者が利用者に許可したくない機能をむやみに利用されないのでセキュリティ上望ましい、といふ場合もあるかもしれません。 (内部DSLであれば必ずしもセキュアであるといふことは意味しません) また、ホスト言語から独立した言語であるため、ほかのホスト言語にまったく同じ仕様のDSLを移植することもできます。

そんなわけでどちらが良いのかと一概に決めることはできないのですが、とりわけRubyは 内部DSLが作りやすい言語 だとされます。 (外部DSLが不得意といふわけではなく、 Racc ユーザマニュアルRuby Parsec といった汎用のパーサジェネレータライブラリが存在し、複雑な外部DSL開発を支援する環境は整ってゐます)

しかし今回作った内部DSLはRuby特有の言語仕様はあまり利用してをらず、(いはゆる)ラムダ式と可変長引数をサポートした言語ならば、容易に移植することができます。(あくまで内部DSLなので、まったく同じものではありませんが)たとへばホスト言語をJavaScriptに移植した例は、次の項で紹介します。

さあ、クロージャの紹介だ。

この項では上記のコードを一行づつ解説することで、ラムダ式とクロージャ(閉包)について解説していかうと思ひます。

はじめにRubyの ラムダ式 について説明させていただきます。

f = ->(x){ 2 * x + 3 } と書くことで、算数の授業で習った f(x) = 2x + 3 といふ函数式と同じものを定義することができます。Rubyではこれを ラムダ式 と呼びます。

f = ->(x){ 2 * x + 3 }
f = lambda{ |x| 2 * x + 3 } # この書き方でも意味は同じ

p f.class
#=> Proc
p f.lambda?
#=> true

Proc とは函数のことだと考へて良いです。正確には procedure / 手続き オブジェクトで、 lambda はその特殊な場合です (f.lambda? で、その Proc オブジェクトが lambda であるかを検査することができます) 。

このコードは f.call(5) または f[5]f.(5) と書くことで計算結果を得ることができます。 f(5) は、 だめです

あれっ、 def を使ってこんなふうに書いた方が良いんじゃないの? と思ったあなたは非常に聡いです。

using_def_f.rb
def f (n)
  2 * n + 3
end

result = f(5)
p result
#=> 13

が、いま欲しいのはメソッドではなく 函数 なので採用しません。

Rubyの def とは何者なのだ? と興味のある方は、むかし書いた Ruby vs Python! ~def vs def~ - DT戦記(zonu_exeの日記) といふ記事がありますので、こちらをお読みください。

では、解説を開始します。

part_scripter.rb
scripter = ->(*args){->{puts args.join("\n")}}

実はここだけ初出時とは仕様が変化してゐます。

part_scripter_origin.rb
scripter = ->(*args){puts args.join("\n")}

これが初出時のコードです。これにもRubyの言語仕様の解説が必要なのですが、 *args と書くことで、 引数をいくつでも取ることができるようになり ( 可変長引数 ) 、そしてその引数は arg という配列に代入されます。

単純な例では、このような函数を作ることができます。

sample_lambda_join.rb
join = ->(*words){ words.join(',') }

puts join['apple', 'orange', 'banana', 'peach']
#=> "apple,orange,banana,peach"

puts join['kiwi', 'papaya', 'mango']
#=> "kiwi,papaya,mango"

さて、次になぜ変更を加へたのか、といふ話をします。

f = ->(str){ str }

このコードを f.call("string") または f["string"] すると、 "string" といふ文字列がそのまま返ってきます。では、次のようにすると?

f = ->(str){
  ->{ str }
}
p f.class
#=> Proc

f_ = f.call("mystring")
p f_.class
#=> Proc

p f_.call
#=> "mystring"

最初の函数 f には ->{} が二重になってゐます (説明が遅れましたが、引数をとらない場合は () を書く必要がありません)。そして、 f.call の結果を f_ に代入すると、 f_ に代入されたものがさらに函数であることがわかります

ここで注目して欲しいのは、引数を渡したのは f なのに、 f_ を呼び出したときまで "mystring" といふ文字列が保存されてゐることです。「えっ、 str って変数に "mystring" が代入されてるんだからそんなのあたりまへじゃないか」と感じるならばおめでたうございます。あなたはクロージャを理解できてゐます!

tsuyoi_part_title-paragraph.rb
title = ->(text){"『#{text}\n"}
paragraph = ->(*args){args.join("\n")+"\n"}

ここは割合普通ですね。特に説明する必要もないかなー、と思ひます。

chara = ->(name){
  ->(text){"#{name}「#{text}」"}
}

ryu = chara[:リュウ]
tak = chara[:たかし]

ここからがクロージャの実践になります。 chara といふ函数にキャラ名を渡すと、函数が返ってきます。その函数にせりふとなる文字列を渡してやると、文字列を整形して返します。

つまり、 chara['はまちちゃん']['こんにちはこんにちは!!!'] とすることで、 "はまちちゃん「こんにちはこんにちは!!!」" といふ文字列が返ってきます。ここで重要なのは「はまちちゃん」といふキャラクターの情報を持ってゐる途中の状態を保存しておける、といふことです。

counter.rb
counter = ->{
  n = 0
  ->{ n += 1 }
}

a = counter[]
b = counter[]

a[]; b[]; a[]; b[];
a[]; a[];

p a[]
#=> 5
p b[]
#=> 3

これはRubyでは一般的な手法ではありません。なぜならRubyはオブジェクト指向言語なので、クラスを定義してオブジェクトを生成する手法がふつうです。つまり、かうします。

counter_using_class.rb
counter = Class.new{
  def []
    @n ||= 0
    @n += 1
  end
  def self.[]; new; end
}
a = counter[]
b = counter[]

a[]; b[]; a[]; b[];
a[]; a[];

p a[]
#=> 5
p b[]
#=> 3

Rubyを使ってこのくらゐシンプルな機能を実現するのにクロージャを利用するか、特異クラスを利用するかは完全に趣味の問題です。特異クラスを使った方が inspect が定義されるぶん、作り込まなくても内部状態を調べやすい、といふメリットはあります。もちろん、拡張性はクラスを利用した方が格段に上です。

script = scripter[
  title[:俺より強い奴に、会いに行く],
  paragraph[
    :ピンポーン,
    ryu[:こんにちは],
    tak[:はいどなた]
  ],paragraph[
    ryu[:いまちょっといいですか], 
    tak[:これから出かけます]
  ],paragraph[
    ryu[:午後出勤ですか?],
    tak[:はい]
  ],paragraph[
    ryu[:強そうですね],
    tak[:なにがですか]
  ],paragraph[
    ryu[:態度が]
  ],paragraph[
    :リュウは、自分より強そうな奴に、会いにいったのだった。 完 
  ]
]

ここが実際にDSLを利用して本文を記述する部分です。全体が scripter[ ... ] で括られてゐて、中身は全て , 区切りの引数になってます。段落ごとに paragraph[ ... ] に渡し、せりふでは ryu, tak 函数にそれぞれせりふを渡します。

なほ、文字列 ( "hogehoge" ) ではなく、シンボル (:hogehoge) といふ形式で書いてるのはRubyをよく知らない人にRubyっぽくないと印象を抱かせることが目的なので、ふつうに文字列で渡して問題ありません。

JavaScriptで強い奴に会ひに行く

と、最後になりましたが、このDSLのJavaScript版を紹介します。 node.js で動きました。

tsuyoi.js
(function(){
var tag = function(){
    var memo = {};
    return function(tagname){
        return memo[tagname] || (memo[tagname] = "#{tagname}".replace("tagname", tagname));
    };
}();
var scripter = function(){
    var args = Array.prototype.slice.call(arguments);
    return function(){
        return console.log(args.join("\n"));
    };
};
var title = function(text){
    return "『#{text}』\n".replace(tag("text"),text);
};
var paragraph = function(){
    return Array.prototype.slice.call(arguments).join("\n")+"\n";
};
var chara = function(name){
    return function(text){
        return "#{name}「#{text}」".replace(tag("name"), name).replace(tag("text"), text);
    };
};

var ryu = chara("リュウ");
var tak = chara("たかし");
var script = scripter(
    title("俺より強い奴に、会いに行く"),
    paragraph(
        "ピンポーン",
        ryu("こんにちは"),
        tak("はいどなた")
    ),paragraph(
        ryu("いまちょっといいですか"), 
        tak("これから出かけます")
    ),paragraph(
        ryu("午後出勤ですか?"),
        tak("はい")
    ),paragraph(
        ryu("強そうですね"),
        tak("なにがですか")
    ),paragraph(
        ryu("態度が")
    ),paragraph(
        "リュウは、自分より強そうな奴に、会いにいったのだった。 完"
    )
);

script();
})();

最後に

えっと、最後にDSLの抽象化のメリットとしてこんなの書いてみたんですけど、そんなにシンプルじゃなくなった気がしてるので、興味あればお読みください。 https://gist.github.com/3968734

127
124
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
127
124