Skip to content

Instantly share code, notes, and snippets.

@ympbyc
Last active March 14, 2019 08:33
Show Gist options
  • Star 37 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ympbyc/5278140 to your computer and use it in GitHub Desktop.
Save ympbyc/5278140 to your computer and use it in GitHub Desktop.
LLerのための関数指向入門

LLerのための関数指向入門

2013 Minori Yamashita ympbyc@gmail.com

ターゲットを動的オブジェクト指向プログラマに絞って、関数指向の考え方を説明します。 コードサンプルでは、オブジェクト指向には CoffeeScript ^1、関数指向には Clojure を使用しますが、文章は汎用的に書いてあります。

最下部に用語集があるので、わかりづらい単語、表現があったら参照してください。

^1: ただしわかりやすさのためにイディオムは少なめ

目次

オブジェクト指向とは

オブジェクト指向と関数指向を比較するにはまずオブジェクト指向の性質を知らなければなりません。ここでは特に、関数指向における関数と見た目が似ているメソッドについて軽くおさらいしましょう。

私がオブジェクト指向を一言で表現するなら「メモリの抽象化」という言葉を使います。オブジェクトは生成時(newされるとき)に、そのオブジェクトのそれぞれのスロット(インスタンス変数)のために割り当てられたメモリ領域へのポインタを内部(this)に蓄えます。 メソッドがスロットの内容を書き換えると、メモリの内容が変わります。

オブジェクト指向以前の言語 - Cでは、サブルーチンにポインタを渡して、指定したメモリ領域を書き換えるといったことがよく行われましたが、オブジェクト指向におけるメソッドはこれのシンタックスシュガーと考えることもできます。

class Animal
  #オブジェクト生成時に呼ばれる
  constructor: (name, age) ->
    this.name = name #メモリに書き込み
    this.age  = age  #メモリに書き込み

  say_hello_to: (whom) ->
    "Hello, #{whom}. I'm #{this.name} of age #{this.age}" #メモリの内容の読み出し

  birthday_party: ->
    this.age += 1    #メモリ内容を変更

#
sam = new Animal "Sam", 5
sam.say_hello_to "stranger" #=> "Hello, stranger. I'm Sam of age 5"
sam.birthday_party()
sam.say_hello_to "stranger" #=> "Hello, stranger. I'm Sam of age 6"

関数指向とは

関数指向は、値(あたい)と関数を使用するプログラミングスタイルです。よく関数型言語と呼ばれる言語は関数指向をサポートします。関数型言語と言うと、遅延評価だの型理論だのモナドだのといきなり刃物の山が降ってくるように感じますが、これらは関数指向の必要条件ではありません。値と関数、この2つのコンセプトだけに的を絞って以降のセクションで詳しく見て行きます。

値ったら最強ね!

オブジェクトでは、データの一部を変更したい時には単にメモリ内容を書き換えてしまうということは既に見ました。Samが6歳になったら5歳の頃のSamは消えてしまうのです。 値にはこれができません。値が持つ情報の一部を変更したいときには、その値を変更するのではなく、変更が適用された新しい値を作ります。

;;この例ではまだ関数は出てきません

(def sam5 { :name "Sam" 
            :age  5 })

(sam5 :age)  ;=> 5

(def sam6 { :name (sam5 :name)
            :age  (+ (sam5 :age) 1)})

(sam5 :age)  ;=> 5
(sam6 :age)  ;=> 6

これならSamが6歳になっても5歳の頃のSamを見ることができます。オブジェクト指向では時間を遡って歴史が書き換わり、値では歴史のタイムラインが作られて行くと考えることができます。プログラミングにおける時間の話に興味のある方は、Rich Hickeyのプレゼンテーションを探してみるといいです。

値のもう一つの利点は、2つ以上のデータ間で、メモリ領域を共有できることです。上の例では、sam6nameスロットはsam5nameスロットを参照しています。 オブジェクト指向では、メモリが書き変わる可能性があるため、複数のインスタンス間で、メモリ領域を共有する事はできません。なので、複数のバージョンを扱いたい場合には、変更前のオブジェクトと同じだけのメモリ領域を新しいオブジェクトのために割り当てて、全構造をコピーしなければなりません。

関数とは

これは簡単です。値を受け取って値を返す値です。関数Fに値Aを渡して結果を得ることを、「AにFを適用する」と言います。

ある関数を同じ値に何度適用しても同じ結果が帰ってきます。

値のセクションで、6歳のSamを作るときにハッシュテーブルリテラルを直接書きましたが、これでは7歳や8歳のSamが欲しくなったときに面倒くさいです。Animalクラスのbirthday_partyメソッドのように処理をまとめる便利なものが欲しくなったら関数を書きましょう。

;birthday_party
(defn birthday-party [anim]
  (conj anim {:age (+ (anim :age) 1)}))

;ついでにsay_helloも
(defn say-hello [anim whom]
  (format "Hello, %s. I'm %s of age %d" whom (anim :name) (anim :age)))

;簡単に動物を作る関数
(defn make-animal [name age]
  { :name name :age age })

(def sam5 (make-animal "Sam" 5))
(def sam6 (birthday-party sam5))

関数は値なので、他の関数を適用したり、他の関数の結果として返却することもできます。JavaScriptのFunctionオブジェクト、RubyのProcオブジェクト等と同じです。

(defn lots-of-parties [animals]
  (map birthday-party animals))

(lots-of-parties [(make-animal "Sam"  5)
                  (make-animal "Bob"  13)
                  (make-animal "John" 200)])

値と名前

関数指向では極力変数を使いません。値に名前を付けるときは、定数か、関数の仮引数名を使います。^1

値のセクションで、値を使うとタイムラインができると書きました。ですがSamが歳を取る度に新しい定数を作っていくのは現実的ではありません。sam5sam6と言った定数をどんどん増やして行く代わりにsamという名前で今見ているSamを指すにはどうすればいいのでしょうか? 関数の仮引数名というのがヒントです。

そう、再帰を使います。^2

(defn life [sam]
  (if (> (sam :age) 100)
      (format "%s has died" (sam :name))
      (do 
        (print (say-hello sam "stranger"))
        (life (birthday-party sam)))))

(life (make-animal "Sam" 0))

life関数の中でSamはどんどん歳を取って行きますが、死ぬまでいつでもsamという仮引数名で参照できます。これはよく使われるパターンなので覚えておきましょう。

^1: いくつかの言語にはrefという便利なものがあるのですが、今回は使いません

^2: clojureで効率よく再帰するときにはloopとrecurという構文を使うのですが、ここではわかりやすさのために普通に再帰します。

関数指向に向いている言語

再帰が効率的にできて、イミュータブルなデータ構造が充実している言語ならだいたい関数指向プログラムを書く事ができます。LLでも、イミュータブルな配列とハッシュテーブルをライブラリとして実装した上で、ポリシーとして変数を使用しない事で関数指向が実現できます。

最初から関数指向をサポートしている言語は

  • Clojure
  • ML系言語 (SML, F#, ...)
  • Haskell

等があります。LLから入る方は、Lispのシンタックスに抵抗がなければ動的型付けのClojureがとっつきやすいでしょう。関数指向は徹底されていないですが、資料が充実しているSchemeで基礎を付けるのも有効です。

【おまけ】 型

関数指向に馴染んできたら、型についての色々な文章を目にすることになるでしょう。型に対する向き合い方は、動的型付けか静的型付けかという違いとはまた別のトピックで、言語によってそれぞれ違います。

クラスベースのオブジェクト指向や、ML、Haskellなどの言語では、あるカテゴリに属するデータを新しく扱うことになったらその度に型を作る文化になっています。本を扱いたければBookクラス/型を、食べ物を扱いたければFoodクラス/型を、その都度作り、それを雛形としてインスタンス、または値を作ります。

Lisp、特にSchemeとClojureでは、今回のコードサンプルもそうですが、リストやハッシュテーブル、ベクタなど、既に用意されている型をそのまま使うことが多いと思います。^1 関数はBook型のような特殊型1つの為に定義するのではなく、リストなら全てのリスト、ベクタなら全てのベクタを受け入れるような関数を定義するのです。Clojureの場合はこれがさらに進んでいて、インターフェースを導入する事で、一つの関数がリストもベクタもハッシュテーブルも簡単に扱えるようになっています。

^1: 型を作る事もできます。

【おまけ】 関数指向 * オブジェクト指向

この記事ではバッサリ省略しましたが、オブジェクト指向には、継承やポリモーフィズムなどの面白いトピックがたくさんあります。これらの機能に慣れ親しんできた方は、いきなり関数指向を押し付けられたら翼をもがれたような気分になるかもしれません。ご安心ください。関数指向と継承、ポリモーフィズムは両立可能です。

Common LispやScheme^1 では昔から、CLOS(Common Lisp Object System)やその変種が使われています。これは名前の通り値ではなくオブジェクトを扱うのですが、部品が分かれているので、値を使うようにすることも簡単です。^2 多重継承、総称関数によるポリモーフィズム、多重ディスパッチなど、先進的な機能が使えます。

Clojureでは、protocolを作って、複数のデータ型に同じ名前の関数を定義することでポリモーフィズムを実現できます。他にもCLOSのdefmethodに似た、defmultiという仕組みもあります。

^1: ただしSchemeでは非標準

^2: define-classは使わず、immutableなrecordを使う等

用語

Sam: 可愛い蛇。

オブジェクト: 内部にメモリを隠蔽し、メッセージを通じてこれを操作するモノ。レコードやハッシュテーブルと呼ばれるデータ構造に近い。この記事ではインスタンス変数とインスタンスメソッドのみを持つものとする。

オブジェクト指向: オブジェクトを使用するプログラミングスタイル。

メッセージ: オブジェクトに対してメッセージを送ると、メッセージを解釈してメソッドが起動される。最近の言語ではメソッドと混同されている。

メソッド: オブジェクト内部のメモリにアクセスできるサブルーチン

サブルーチン: 処理をひとまとめにして名前をつけたもの。引数を取る事ができる。

メモリ: 高レベルな言語では直接操作することはないが、プログラム実行中にオブジェクトやデータが置かれる場所。巨大な配列をイメージしても良い。

ポインタ,参照: この記事の範疇ではメモリ番地と解釈して問題ない。配列のアナロジーでは、添字(インデックス)を指す。

データ: オブジェクトや値をひっくるめて筆者が便宜的に使用している単語。メモリに存在し得るもの全般を指す。

関数: 値を取り、値を返す値。

関数指向: 関数と値を使用するプログラミングスタイル。

: 数値、文字列、リストなどのイミュータブルなデータのこと。変更不可。字面が同じ値はidentical。オブジェクトやミュータブルなデータは値ではない。

ミュータブルなデータ: メモリが直接書き変わるタイプのデータ。オブジェクトはミュータブル。一度ミューテート(mutate、mutableの動詞形)すると、元のデータにはアクセスできなくなる。字面が同じでもidenticalであるとは限らない。

イミュータブルなデータ: メモリ内容を変更できないタイプのデータ。 = 値

ハッシュテーブル: キーと値を結びつけるデータ構造。キーによる検索に特化している。 (TODO: expand more)

とりあえず

以上になります。 関数指向プログラムを書くのに必要最低限な概念は一通りカバーできたと思います。

もっと突っ込んだ話は他のソースにお任せします。私が勉強する過程で読んだ物のうちいくつかをここにまとめてあります。よかったら参考にしてみてください。

ご意見ご感想は ympbyc@gmail.com もしくは @ympbyc までお寄せください。

ちなみにgistってプルリクエスト送れるらしいですよ。

追記

質問を頂いたので回答します。関数指向という言葉ですが、関数型という言葉と完全に同義には使っていません。本当はvalue-oriented、値指向という言葉を使いたいのですが、これだと聞き覚えがなさ過ぎて読んでもらえなさそうだったので関数指向という言葉を導入する事で手を打ちました。

参照

@ympbyc
Copy link
Author

ympbyc commented Jun 11, 2013

はい+は関数です。 正しくは 関数定義は出てきません です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment