このエントリーをはてなブックマークに追加
はてなブックマーク - 関数型の記事に出てる命令型のコードがコレジャナイ感満載な件について
Bookmark this on Digg

どうも!クックパッドのエンジニアに業務命令で焼きそばを焼かせたらやっぱり会社辞めちゃうのか、私、気になります!

余談ですが、会社や上司に不満があれば、たとえ小さなことでも不満材料になるんだなというのは勉強になりました (焼きそばが小さいことかどうかは別として)。それに「会社批判のツイートを強制的に消された」「非正規雇用者に甘い言葉をかけておきながら使い捨て」「総務部は追い出し部屋」と具体的に書かれてるのに「本質的な問題とは思わない」と一蹴する社長、ステキすぎです!よくこういう人をヨイショする気になるよなー。

ところで「社畜」ってのは、長時間働かされることよりも、会社や上司への不満を言えない状態のとこを指すような気がしてきました。ツイート消すよう圧力かかるぐらい言いたいことも言えないこんなドワンゴじゃポイズン!

某雑誌記事にあった命令型のサンプルコードがコレジャナイ感

さて、関数型プログラミングを特集した雑誌の記事に、こんなサンプルコードが載ってました:

int z = 0;
int f(int x){
     z = z + x;
     return z;
}

そして、このコードは『状態を持ち、副作用を簡単に生じさせてしま』うけど、関数型なら副作用がないコードになるから関数型のほうがよい、という論調でした。

けど、こんなあからさまなグローバル変数の読み書き、使います?まあ使う人もいるでしょうが、ふつうはこんなコード、書かないよね1 (計算コストの高いデータをキャッシュするときはこれに近いことをやるけど、その場合も並列に呼び出されても問題ないように書きます)。こんな不自然なコードを持ち出して「関数型のほうがバグが少ない!」と言われても、お前の中ではそうなんだろうお前の中ではな、ぐらいしか感想が浮かばない。「サンプルコードだから」と言われそうだけど、サンプルコードだからこそ短いながらも説得力のあるコードを示すべきでした。

それより、こんな不自然なグローバル変数を書き換えるサンプルじゃなくて、ローカル変数の書き換えがバグへとつながるサンプルを説明してほしいです。関数型は (グローバル変数どころか) ローカル変数への再代入すら副作用と見なして排除しようとしてるけど、ローカル変数の書き換えってそんなに邪悪でしょうか? グローバル変数の書き換えがバグにつながるのは分かるけど、ローカル変数まで同じように扱う理由が未だによく分かりません2

そりゃ専用の関数を使えば短くなるに決まってるだろ

何も雑誌に限ったことではありません。関数型と命令型とを比較するとき、恣意的なコードを使って「関数型すごい!」という主張をする人はけっこういます。

たとえばこちらの記事から引用します:

(例)自然数 1 から n までをすべて足しあげた結果を求める問題

/* C言語 */
int totalFromTo(int n) {
    int result = 0;
    for (int i = 1; i <= n; result += i++);
    return result;
}

-- Haskell
totalFromTo = sum . enumFromTo 1

このような例をもって『Haskell のコードがより短時間・より短い行数で書ける』と主張してるんですが…、ちょっと待て、そりゃ sum のような専用の関数を使えば短くなるに決まってるだろ! sum を使っていいならたとえばPythonでも短く書けるぞ。

def totalFromTo(n):
    return sum(range(1, n+1))

そもそも、この例なら return n * (n+1) / 2; と書けばC言語でも短時間かつ短い行数で書けてしまうので、比較としてはあまりよい例ではないでしょう (関数合成の例としてならまだ分かりますが)。

関数型な人は関数型以外をろくに知らないんじゃないか疑惑

他の例。たとえばこちらの発表資料だと、こんなRubyコードが書いてあります:

def func (ar)
  sum = 0;         ← 命令の列挙
  i = 0;           ← 命令の列挙
  ar.each{|x|
    sum += x * i;  ← 破壊的代入
    i += 1;        ← 破壊的代入
  }
  sum;
end

でも、Rubyでこんなコード書かないよねぇ?セミコロン満載なのは大目に見るとしても、インデックスつきの繰り返しがしたいならせめてこう書くとか:

def func(ar)
  sum = 0
  ar.each_with_index {|x, i| sum += x * i }
  sum
end

あるいは inject()3 を使っていいなら:

def func(ar)
  ar.each.with_index.map {|x, i| x * i }.inject(:+)
end

いちおう資料には map & reduce 版もあるんだけど、これがまた微妙なコードなんだよね。資料より引用すると:

def func (ar)
  ar.zip((0..ar.length).to_a) \
    .map{|(x,i)|x*i} \
    .reduce(:+);
end

この人、セミコロン好きすぎだろ。それに .to_a|(x,i)| は無駄でしょ。これでいいよ:

def func (ar)
  ar.zip(0..ar.length).map{|x, i| x*i}.reduce(:+)
end

こんなコードを出して「命令型より関数型!」と言われても、説得力ないよね。これがたとえば「Rubyでのメソッドチェーンは、Haskellなら関数合成を使えば似たようなのが書けます。だからRubyistもHaskellをやってみよう!」という説明なら、わかる。けどこんなサンプルで「関数型は小さな関数をつなぎ合わせるからバグが入りにくい!」と言われても、「Rubyでも小さなメソッドをつなげるんだけど?」という感想。

またこちらの資料の p.24 には、「Javaで不格好なfor」というタイトルでこのようなコードが紹介されてます。

public static int func(int[] ar) {
  int ret = 0;
  for (int i = 0; i < ar.length; i++) {
    ret = ret + ar[i] * i;
  }
  return ret;
}

まあ2013年4にもなって拡張for文を使わないで書いてたら、そりゃ不格好にもなるよね…。

それにこれ、ret = ret + ... はちょっとズルくない? ret += ... と書いてあげればいいのに、わざと長くなる書き方をして不格好さを演出してるようにみえます。

自分が書くなら、こうかなあ。これなら、別段不格好だとは思わない。また ret という変数名も微妙だから内容を表す名前にするかな:

public static int func(int[] ar) {
  int i = 0, sum = 0;
  for (int x: ar) sum += x * i++;
  return sum;
}

不自然な関数型コードより自然な命令型コード

さらに別の記事。POSTD: 関数型プログラミング入門から引用しますが、命令型で愚直に書かれたこのコードが:

from random import random

time = 5
car_positions = [1, 1, 1]

while time:
    # decrease time
    time -= 1

    print ''
    for i in range(len(car_positions)):
        # move car
        if random() > 0.3:
            car_positions[i] += 1

        # draw car
        print '-' * car_positions[i]

手続きに分割して書き直すと、こうなるんだそうです:

from random import random

def move_cars():
    for i, _ in enumerate(car_positions):
        if random() > 0.3:
            car_positions[i] += 1

def draw_car(car_position):
    print '-' * car_position

def run_step_of_race():
    global time
    time -= 1
    move_cars()

def draw():
    print ''
    for car_position in car_positions:
        draw_car(car_position)

time = 5
car_positions = [1, 1, 1]

while time:
    run_step_of_race()
    draw()

そして、後述する関数型バージョンのほうがこれよりも良いコードだと言ってます。

けどな、ふつうはこんな無意味なグローバル変数を使うわけないだろ。せめてこう書いてくれ:

## 上のクソコードを書き直した例
from random import random

def run_race(car_positions, time):
    for _ in range(time):
        move_cars(car_positions)
        draw_cars(car_positions)

def move_cars(car_positions):
    for i in range(len(car_positions)):
        if random() > 0.3:
            car_positions[i] += 1

def draw_cars(car_positions):
    print('')
    for car_position in car_positions:
        print('-' * car_position)


def main():
    car_positions = [1, 1, 1]
    time = 5
    run_race(car_positions, time)

main()

こんなのにグローバル変数を使う必要なんかないです。関数型を持ち上げるために手続き型で不自然なコードを使うの止めろ。ちっとも公平な感じがしない

そして、関数型で書くと次のようになるらしいです(記事から引用):

from random import random

def move_cars(car_positions):
    return map(lambda x: x + 1 if random() > 0.3 else x,
               car_positions)

def output_car(car_position):
    return '-' * car_position

def run_step_of_race(state):
    return {'time': state['time'] - 1,
            'car_positions': move_cars(state['car_positions'])}

def draw(state):
    print ''
    print '\n'.join(map(output_car, state['car_positions']))

def race(state):
    draw(state)
    if state['time']:
        race(run_step_of_race(state))

race({'time': 5,
      'car_positions': [1, 1, 1]})

この関数型のコード、分かりやすいですか? Pythonの関数型としての能力がそれほど高くないことを差し引いても、分かりやすいコードとはとても思えません。

この例題は、どう考えてもふつうにオブジェクト指向で書いたほうが分かりやすいです:

## オブジェクト指向で書き直し
from random import random

class Car(object):

    def __init__(self, position=1):
        self.position = position

    def move(self, distance):
        self.position += distance

class Race(object):

    def __init__(self, cars):
        self._cars = cars

    def run(self, time):
        for _ in range(time):
            self.move_cars()
            self.draw_cars()

    def move_cars(self):
        for car in self._cars:
            if random() > 0.3:
                car.move(1)

    def draw_cars(self):
        print('')
        for car in self._cars:
            print('-' * car.position)
        ### or:
        #buf = ["\n"]
        #for car in self._cars:
        #    buf.append("-" * car.position + "\n")
        #return "".join(buf)

##

def main():
    cars = [ Car() for _ in range(3) ]
    time = 5
    Race(cars).run(time)

main()

関数型な人はよく「オブジェクトの状態を変化させるからオブジェクト指向はバグが入りやすい」と言っています。しかし (2つ上のような) 関数型スタイルで書かれた不自然なコードよりも、オブジェクト指向で自然に書いたコードのほうが、自分にとっては読みやすいしバグも入りにくいです (もちろんそうじゃない人もいるでしょう)。

(勘違いする人がいそうだから念を押しておくけど、「関数型で書いて不自然になるくらいなら命令型やオブジェクト指向で自然に書いたほうがいい」という話であって、「関数型で書くとみな不自然」と言ってるわけじゃないからな!そのくらいの読解力は持ってくれよ!)

おわりに

  • 関数型と比較するなら、命令型のコードはもうちょっとまともなものを用意してください。恣意的な比較はやめましょう。

  • 副作用を追い出したり局所化するのはわかるんですけど、ローカル変数の書き換えまで副作用として排除する理由がよくわかりません。

  • 「関数型プログラミングとは、副作用を極力排除したスタイル」と言いながら、関数型の利点として「静的な型チェックが強力」とか「パターンパッチが強力」とかを持ち出されると、混乱します。持ち出すなら、副作用を極力排除したことによる利点にすべきであり、それ以外の利点は言語固有のものとして分けてほしいです。

  • 「関数型って何?」と聞いたら「実は定義がはっりしてなくて、人によってバラバラなんだよね〜」とはぐらかしておきながら、誰かの説明に少しでも間違いがあると「その説明は関数型として正しくない!」と取り締まるの、矛盾してませんか?関数型警察こわい。

  • 関数型がなかなか理解されないのを「命令型やオブジェクト指向に慣れてると関数型は理解しにくい」とごまかすの、やめてください。わかりやすい説明ができてないだけです

  • オブジェクト指向の本質とは、名前をつけて対象を識別し、それを扱うこと』などというトンデモ解説は岡部度が高すぎます。そういう説明で比較をしようとするから関数型が理解されないんじゃないでしょうか。(関数型が理解されないのは説明する人が命令型やオブジェクト指向に慣れてないから説)

  • カプセル化って関数型だとどうやるんですか?』という素朴な疑問に対し、『Haskellの例で言えばデータ型が不変なのでカプセル化が必要ない』という的外れな回答5しかもらえないうえに、『見ていられないので書いちゃいますね。棋士にとって、詰んでいることに気づくことなく将棋を進めてしまうことほど恥ずべきことはないらしいですよ。知ってました?』などと上空200メートルからの上から目線で言われてるのを見たら、やっぱり関数型言語は理解されないままだろうなと思いました。記事本文がとても参考になるだけに、ねえ。

  • 「オブジェクト指向だってちゃんとした定義がないのに広まってるじゃないか!」という向きもあるようですが、オブジェクト指向言語は広まっていてもオブジェクト指向が広まってるとは思わないでください。現に、これだけの記事を書けるような人でもカプセル化を分かってないじゃないですか。

  • 「その定義だとXXXという言語が関数型に含まれなくなる、だからその定義は間違い」という主張はわかるんですが、その「XXX」にLispやSchemeが入らないのは、長年関数型として紹介されてきた言語なのにどうしてそうなった?感があります。言語XXXが入れるように関数型の定義を決める一方で、その定義に当てはまらないという理由でLispとSchemeが関数型から排除されるのは、ちょっと不自然 (LispやSchemeも含まれるような定義にしたらいいのにと思っちゃう)。

  • 関数型がなかなか理解されないからといって「命令型やオブジェクト指向に慣れてると関数型は理解しにくい」とごまかすのは、かっこ悪いです。わかりやすい説明ができれば、比較対象があったほうが本来はわかってもらえやすいはずです。(大事なことなので2回言いました!)

  • 話の主題には触れず、主題じゃないところで延々と噛みつくの、不毛すぎです。


  1. このコードはマルチスレッド下で破綻するのは明らかです。もし書くなら、ロックしたり何なりしましょう。 

  2. プログラムコードの証明がしたい人には、ローカル変数の書き換えはすごく邪魔らしい。でもOCamlもHaskellも証明書いてるわけじゃないしなあ。 

  3. Ruby の Enumerable#inject() は #reduce() の別名。 

  4. 資料が書かれたのが2013年らしいので。 

  5. なぜ的外れかは、Wikipediaの「カプセル化」のページを見ればわかるでしょう。