プログラミング言語処理入門 (YAP(achimon)C::Asia Hachioji 2016 没スライド)

Post on 15-Apr-2017

2.107 views 0 download

Transcript of プログラミング言語処理入門 (YAP(achimon)C::Asia Hachioji 2016 没スライド)

プログラミング言語処理入門以前 Esehara shigeo

お前誰だ

esehara shigeo (32)twitter: @esehara

プログラミング言語オタク

概要

発表の概要

1. プログラミングについて知るためには、実際に簡単な処理系を

作ってみるのが手っとり速い

2. 一番簡単な構文はLispなので、Lisp処理系を作る

3. ただ、そのまま作ると余りにも重いので、一日でできるくらいミ

ニマムな奴を考える

4. できたのでUnLispと名づけた次第

方針

リストはUnlispを実装する言語の

文字列リストが

ネストした形のものであること

["do", ["def", "fib", ["fn", "n", ["if", ["=", "n", "0"], "0", ["if", ["=", "n", "1"], "1", ["+", ["fib", ["-", "n", "1"]], ["fib", ["-", "n", "2"]]]]]]], ["fib", "7"]]

仕様の理由

1. カッコの対応関係を調べるのが面倒であること

2. このような実装の方針を取っている奴はいくつか

ある

3. 最低限ということを考えた場合、テキストからリス

トを作るのは余技であると判断

仕様の理由

1. カッコの対応関係を調べるのが面倒であること

2. このような実装の方針を取っている奴はいくつか

ある

3. 最低限ということを考えた場合、テキストからリス

トを作るのは余技であると判断

実例

達人出版社から無料配布中

『つくって学ぶプログラミング言語』より

miniMAL ( https://github.com/kanaka/miniMAL )

この方法のメリットとデメリット

1. プログラミング言語のリテラルを使うことができるので字句解

析をしないで済むことができる。だたし、字句解析の部分は言

語処理でやっていて越したことはないので、これはちょっと簡

略化しすぎている印象は否めない

2. ほとんとのビルドイン関数を、プログラミング言語に搭載したビ

ルドイン関数に丸なげできる。しかし、そうなると、実装自体

が、プログラミング言語のメタプログラミングになるので、汎用

性が無くなる

3. とはいえ、手っとりばやさはこの方法が一番強い

先を踏まえた上でのUnlispでの方針

なので、Unlispでは「文字列」のリストを採用し、そこ

からトークンを作成し、それを元に構文解析を行うと

いう方針を取る。そうすることによって、とりあえず字

句解析と、構文解析という二つの方法を分割するこ

とが可能になる。

なんで「実装言語のリストを利用するの?」

データはコード

例: Ansible(プロビジョンツール)によるループ

例: TeXによるFizzBuzz

http://hogespace.hatenablog.jp/entry/2012/02/20/133731 より

%residue(剰余)の結果

\newcount \ResidueResult

%剰余を返すマクロ

\def \residue #1 #2{

\newcount \tmp

\newcount \tmpb

\tmp=#1

\tmpb=#1

\divide \tmp by #2

\multiply \tmp by #2

\advance \tmpb by -\tmp

\ResidueResult=\tmpb

}

\def \FizzBuzzUnit#1{

\newcount \FizzFlag

\newcount \BuzzFlag

\newcount \AllFlag

\FizzFlag=0

\BuzzFlag=0

\AllFlag=0

\residue #1 3

\ifnum \ResidueResult=0

\FizzFlag=1

\fi

\residue #1 5

\ifnum \ResidueResult=0

\BuzzFlag=1

\fi

\advance \AllFlag by \FizzFlag

\advance \AllFlag by \BuzzFlag

\ifnum \FizzFlag=1

Fizz

\fi

\ifnum \BuzzFlag=1

Buzz

\fi

\ifnum \AllFlag=0

#1

\fi

}

で、何が言いたいの?

このようにして考えた場合、「データが何であるか」と

いう問いに意味はない。自分の意見では、データに

意味を与えているのは、「データをどのように処理す

るか」という処理系の側である。とするならば、「実装

を最低限にはするが、プログラミング言語処理のト

ピックを拾いあげる」とする目的において、配列を使

うという方法は、それなりの合理性があると考える。

処理

『How To Create Your Own Freaking Awesome Programming Language』より

方法

とりあえずどんな方法があるか見てみる(ANTLR)

とりあえずどんな方法があるか見てみる(RAGEL)

とりあえずどんな方法があるか見てみる(Treetop)grammar Arithmetic rule additive multitive ( '+' multitive )* end

rule multitive primary ( [*/%] primary )* end

rule primary '(' additive ')' / number end

rule number '-'? [1-9] [0-9]* endend

ライブラリ使うのか問題

今回はもうちょっと軽量化したい

ため、これらのツール(ライブラ

リ)を使うのは一回避ける

とりあえずどんな方法があるか見てみる(正規表現)func patterns() []Pattern {

return []Pattern{{whitespaceToken, regexp.MustCompile(`^\s+`)},{commentToken, regexp.MustCompile(`^;.*`)},{stringToken, regexp.MustCompile(`^("(\\.|[^"])*")`)},{numberToken, regexp.MustCompile(`^((([0-9]+)?\.)?[0-9]+)`)},{openToken, regexp.MustCompile(`^(\()`)},{closeToken, regexp.MustCompile(`^(\))`)},{symbolToken, regexp.MustCompile(`^('|[^\s();]+)`)},

}}go-lisp( https://github.com/janne/go-lisp/blob/master/lisp/tokens.go )より

Unlispの字句解析の方針について

いろいろな方法はあるけれども、

とりあえずは「正規表現」で良さ

そう(とりあえずtoken化を最低限

できればよい)

こんな感じ

module Unlisp module Lexer def tokenize str return Token.new(Token::LIST, str) if str.is_a? Array case str when /\d+/ Token.new(Token::INTEGER, str.to_i) when /(\D.*)/ Token.new(Token::ATOM, $1) else Token.new(Token::ERROR, str) end end

def list_analyzer lst parse_result = [] while !lst.empty? token = tokenize(lst.shift) if token.list? token.value = list_analyzer(token.value) end parse_result << token end parse_result end end

module Lexer extend self endend

トークンの中身はいたってシンプル

要するに、トークン

の種類と、そのトー

クンの値さえあれ

ば、なんとかなる。

module Unlisp class Token attr_accessor :type, :value, :env

# Token types INTEGER = 1 # STRING = 2 ATOM = 3 LIST = 4 FUNCTION = 5 ERROR = 6 # ... endend

あれ、String型はサポートしないの

面倒だからですちゃんとした理由としては、型が増えれば、それに対する操作も当然に増える。StringがあればStringの操作も必要になる。そうすると実装する範囲も増えるので、これはよろし

くはない。だたし、Hello, Worldできない言語は言語じゃない(重要)なので、そのあたり

のサポート関数はあったほうが良い。

あれ、Boolean型はサポートしないの

面倒だからですちゃんとした理由としては、BooleanはIntegerで代用が可能であるから。(例:C言語、

Python)

Booleanは結局Integerで代用できる例(Python)

def true_or_false(x): if x: print("This is True.") else: print("This is False.") true_or_fales(0)true_or_false(1)

怠惰なのでヘルパーメソッドは生やす(美徳)

def self.false Token.new(INTEGER, 0) end

def self.true Token.new(INTEGER, 1) end

def false? integer? && value == 0 end

def true? integer? && value != 0 end

構文

演算子の優先順位問題

(* OCamlの場合 *)sqrt 10.0 +. sqrt 10.0;;(* - : float = 6.32455532033675905 *)

# Rubyの場合

Math.sqrt 10 + Math.sqrt 10# SyntaxError: unexpected tINTEGER, # expecting end-of-input

で、これって何がおきてるの?

Rubyの場合、Math.sqrt(10 + Math.sqrt) 10といった

ような、オペレータの結合優先度が高いため、エラーとな

る。実際に、Math.sqrt 10 + 10だとエラーは起きない。

だが、このときMath.sqrt(10 + 10)なのか、それとも

Math.sqrt(10) + 10 なのかは判断が付かない。(ただ

し、このコードは極端な例。実際に書くと『リーダブルコー

ド』で殴られる可能性はある)

演算子の優先順位問題(再び)

3 * 4 + 5 * 6# In Ruby => 42

3 * 4 + 5 * 6. "In Smalltalk => 102"

Smalltalkのクラスの中身

演算子の優先順位解説

Smalltalkの場合、Integerのメソッドとして演算子は定義されて

いるので、容赦なく左に結合していく。(わかりやすく言うと(((3 * 4) + 5) * 6)に結合していく)

「人間がこのように規則を適用するのが自然である」ということを

実装するのは、結構問題がある。例として考えるならば、計算式

の順序としては「式 = { (式), *, / , +, - } 」という順序が作れる。計

算式ならば、それほど難しくはないが、このような構文の適用順番

というのは常に考えないといけなくなる。

面倒

なんで最初に「Lispっぽいもの」を実装するといいのか

Lispの構文の場合、先頭に関数、後者に引数ということ

のみ考えればいいので、上記の問題は発生しない(そう

いう意味では、Smalltalkの場合も同様のことが言えるか

もしれない。ただ、Smalltalkの場合、オブジェクト指向と

いう別の問題が出てくる)

例: (hoge 1 (+ 1 2 ) 3)

蛇足

個人的な意見

優先順を決めることにおいてすら、そのプログラミング言

語自体の設計思想において演繹される。確かに、「3 * 4 + 5 * 6」を、いわば「(3 * 4) + (5 * 6)」、あるいは「(+ (* 3 4) (* 5 6))」とするのは、人間の慣習というよりは、機械

への歩みよりということができるが、逆にそれは機械に

とっても、人間にとってもほどよく優しいことがある。

個人的な意見

「明示的に計算順位を記述すること」と、「暗黙的に処理

系が計算順位を決定して欲しい」とする問題は、一種の

トレードオフと言えるわけで、後者の場合においては、人

間が「その処理系の気持ちになって考える」という側面

が出てきてしまう。ただ、通常使う場合においては、後者

のほうが支持される傾向にありそうである。

個人的な意見(例えばPHP)

例えば、"1"というString型を、わざわざInt型にするのは

まどろっこしいという怠惰がある場合、PHP(またはPerl)では"1" + "1"で、2として計算できる。これは、人間にとっ

て「1」というのは、文字列でもあり、数字でもあるというと

ころから考えれば、解らなくもない理屈である。ただ、こ

のような闇雲なキャストを行うということは、逆に機械側

の負担を大きくし、バグを引きよせる原因となる。

問題

引数は関数に渡される前に評価されるのが通常

def my_if(predicate, true_case, false_case): if predicate: return true_case else: return false_case

def counter(n): return my_if(n == 10, n, counter(n + 1))

無限ループを避ける(無名関数でラップする)

def my_if(predicate, true_case, false_case): if predicate: return true_case else: return false_case()

def counter(n): return my_if(n == 10, n, lambda: counter(n + 1))

分岐(if)の実装方法について

普通、ifを実装する場合において、何らかの

形で、ある分岐を採用しない場合は、その

部分の式、あるいは文を評価することを遅

らせなければならない

Smalltalkの場合を見てみよう(Fizzbuzz)((1 to: 100) collect: [ :i | (i % 15 == 0) ifTrue:'FizzBuzz' ifFalse: [(i % 5 == 0) ifTrue: 'Buzz' ifFalse: [(i % 3 == 0) ifTrue: 'Fizz' ifFalse: i asString ]]]) do: [:i | Transcript show: i asString; cr].

SmalltalkのifTrueというメソッドの定義ってどうなってる

ifFalse: falseAlternativeBlock ifTrue: trueAlternativeBlock ^falseAlternativeBlock value

※これはFalseの場合。要するに、ifTrueのブロックを破棄して、

ifFalseを採用しているだけだということがわかる

Smalltalkの場合、メソッドなので当然評価される

|i|(1 = 1) ifTrue: (i := 10) ifFalse: (i := 20).Transcript show: i.

※このとき、iに代入されている値は20

なので、評価を一回遅らせるためブロックにする

|i|(1 = 1) ifTrue: [i := 10] ifFalse: [i := 20].Transcript show: i.

※このとき、iに代入されている値は10

Rubyに移植してみよう

class TrueClass def ifTrue yield end def ifFalse self endend

class FalseClass def ifTrue self end def ifFalse yield endend

class Object def ifTrue self end def ifFalse self endend

RubyでSmalltalkを真似てみよう(Fizzbuzz)1.upto(100).each do |n| (n % 15 == 0) .ifTrue { puts "FizzBuzz" } .ifFalse { (n % 3 == 0 ) .ifTrue { puts "Fizz"} .ifFalse { (n % 5 == 0) .ifTrue { puts "Buzz" } .ifFalse { puts n} } } end

元々評価が遅延する場合(Haskell)

if' :: Bool -> a -> a -> aif' True x _ = xif' False _ y = y

※実は、この定義はラムダ計算におけるTrueとFalseと同様なのだが、詳細は省く。詳し

くは( https://wiki.haskell.org/If-then-else ) を参考にするとわかりやすい

概要

Unlispの実装コード自体はそれほど面白くないから、ここでは省く

けれども、要は["if", ["<", "3", "2"], ["+", "x", "x"], ["+", "2", "2"]]というコードが入ってきた場合、評価自体は2番目のformが

定まるまで保留し、trueであった場合は3番目を評価メソッドで評

価し、falseである場合は、4番目を評価するといった流れになって

いる。このように、分岐する場合においては、処理を保留しなくて

はならない。

複数の条件分岐に関してはifの入れ子として表現する

["do", ["def", "fib", ["fn", "n", ["if", ["=", "n", "0"], "0", ["if", ["=", "n", "1"], "1", ["+", ["fib", ["-", "n", "1"]], ["fib", ["-", "n", "2"]]]]]]], ["fib", "7"]]

複数の条件分岐に関してはifの入れ子として表現する

["do", ["def", "fib", ["fn", "n", ["if", ["=", "n", "0"], "0", ["if", ["=", "n", "1"], "1", ["+", ["fib", ["-", "n", "1"]], ["fib", ["-", "n", "2"]]]]]]], ["fib", "7"]]

複数の条件分岐を表現する構文

def fizzbuzz(x) puts (if x % 15 == 0 "FizzBuzz" elsif x % 3 == 0 "Fizz" elsif x % 5 == 0 "Buzz" else x ) endend

(define (fizzbuzz x) (println (cond [(= (modulo x 15) 0) "FizzBuzz"] [(= (modulo x 3) 0) "Fizz"] [(= (modulo x 5) 0) "Buzz"])))

def fizzbuzz(x): if x % 15 == 0: print("FizzBuzz") elif x % 3 == 0: print("Fizz") elif x % 5 == 0: print("Buzz") else: print(x)

複数の条件分岐を実装しないのはなぜか

先ほど述べたように、条件分岐を行う場合、評価順

を考慮しなければならない。複数の条件分岐をその

まま実装するのは、そのような評価順を考慮する

ルールが増えるのでよろしくない。例えば、SICPとい

う書籍においては、Schemeにおけるcondをifに展

開するようにしている。これが示すのは、複数の条件

分岐はたかだかifの重ね合わせであるということだ。

関数

関数を定義しないとおもちゃとして面白くない

というわけで、構文のことを長々と話したわけだけれども、実際の

ところ、関数が定義できないと、おもちゃとして面白くない。

従って、関数を定義できるようにするわけだけれども、Unlispにお

いては、変数を定義する defと、無名関数を定義するfnしか用意

していない。またfnに関しても、一引数しか使えないようになって

いる。

直接「名前付き関数」が定義できないのはどうしてか

そもそも「名前付き関数」自体

が、変数に無名関数を代入した

ものの糖衣構文だと考えること

ができる。

Schemeでの実例

;; 名前つき関数

(define (square x) (* x x))

;; ラムダで定義する

(define square (lambda (x) (* x x)))

OCamlでの実例

(* 名前つき関数 *)let square x = x * x

(* ラムダで定義する *)let square = fun x -> x * x

JavaScriptの事例

// 名前付き関数

function foo(x) { return x * x ; }

// 無名関数

var foo = function(x) { return x * x ; }

直接「名前付き変数」が定義できないのはどうしてか

さて、このように考えた場合、関数定義を直接行うという方針につ

いては、実際のところ、なんらかの変数に対して、無名関数を束縛

するという考え方で代用することが可能である。

もっと言ってしまうならば、変数に対して無名関数をオブジェクトと

して代入(あるいは束縛する)ということこそが、関数宣言である

と、乱暴にまとめてしまうことが可能である。

無名関数が引数を実質一つしか取れないのは何故?

引数関数は、実際のところ、あるn引数関数

を1引数関数として表現することをCurry化と呼ぶ。

無名関数が引数を実質一つしか取れないのは何故?

Curry化を持ちいると、2引数関数、3引数関数……n引数関数は、1引数関数の入れ子構造になる。『論

理と計算のしくみ』によれば(p192)、「関数の計算に

関して純粋な理論を展開するには、関数はすべて1引数であると仮定してよいように思われる」としてい

る。

実例(JavaScript)// 通常の関数

var noncurry = function(x, y) { return x + y }noncurry(1, 2);// カリー化された関数

var curry = function(x) { return function(y){ return x + y } }curry(1)(2);

実例(Python)

def no_curry(x, y): return x + y

no_curry(1, 2)

def curry(x): def inner_curry(y): return x + y return inner_curry

curry(1)(2)

実例(Haskell)

curry :: Int -> Int -> Intcurry x y = x + y

plus_one x = curry 1

※Haskellの場合は、関数はcurry化されているので、こういう風に宣言することが可能

実例(Ruby)

curried = ->(x, y){ x + y }curried[1, 2]

# RubyにはCurry化のメソッドがある

curried.curry[1][2]

環境

大域環境と小域環境

簡単に言ってしまえば、大域環境はグ

ローバル、つまり何処からでも参照でき

る環境である。例えば、普通に関数を定

義した場合、小域環境に、その関数の

情報が作られる。

その一方で、関数に対して入ってくる引

数を一時的に格納する変数が存在して

いる。

# echoは大域環境

def echo(x): # xは小域環境に入る

print(x)

# 3がxが入る

echo(3)

小域環境とは何か

このxの値は、関数を抜けた場合には無効である必

要がある。なぜならば、このxは関数の内部におい

て、利用するために一時的に付けられている名前に

過ぎないからである。とすると、大域環境とは別に、

このような一時的に利用される関数の環境も用意し

なければならない。

Unlispを使って考えてみる

[["fn", "x", ["+", "x", "1"]], "6"]

このとき、引数に6が渡されているわけだが、このとき、"6"という数

字をxに適用する。すると、この段階で、環境として、["+", "x", "1"]を評価するさいに、小域環境として、x = 6という、ローカルな

環境が新しく作られ、それに従って評価される。

実際の実装について

実際の環境の実装は、変数名と値(あるいは関数宣言

のコード)を一緒に渡すかたちになっている。

上の関数の場合は、[["x", 6]]という形で、環境に対し

て、。このとき、この式が評価される場合には、`"x"`が入ってきたときには、環境に保存されたペアを検索し、そ

の値を起きかえる。

わかりやすい図(SICPより)

わかりやすい図(SICPより)

実際の実装

関数に入ったときに新しい環境を作成し、離脱するときにさし戻すようにする。

def call lst, env # ... head = lst[0] head.env = head.env.next [head.value[0], next_val(lst)] if head.value[1].list? result_lst, _ = list_eval(head.value[1], head.env) elsif head.value[1].atom? result_lst, _ = apply_atom(head.value[1], head.value, head.env) end return result_lst, env end

クロージャ

乱暴に言えば、関数が定義された小域環境を、関数が定義されたときに保持する。

Unlispの場合:

[[["fn", "x", ["fn", "y", ["+", "x", "y"]]], "2"], "3"]