RubyのProcでのCurry化、部分適用(partial application)など


はじめに

Rubyでは、eachmapなどを使う時、下記のような書き方があります。

strs = %w(1000 1001 1010)
strs.map do |s|
  s.to_i
end
# OR
strs.map{ |s| s.to_i }
# OR
strs.map(&:to_i)

しかし、.to_i(2)など、要素自身以外の変数を代入しようとする時、strs.map(&:to_i)の書き方が書けないのは少し不便でした。

ちょうど最近、私はPythonを触っていて、関数の部分適用(partial application)からヒントを得ました。
例えば上記Rubyの例をPythonにすると

strs = ['1000', '1001', '1010']

import functools
int_bin = functools.partial(int, base=2)

[int_bin(s) for s in strs] # [8, 9, 10]
# Or
list(map(lambda s:int_bin(s), strs))
# Or
list(map(int_bin, strs))

上記Pythonコードでは、int_binというint(s, base=2)の部分適用を作り出し、レシーバーとしてmapに渡しました。

mapに任意のBlockやProcを代入する

下記の方法を使います。
解説はこちらの記事を読んでください。

# Proc を使う場合
to_i_bin = ->(s, base=2) { s.to_i(base) }

strs = %w(1000 1001 1010)
strs.map(&to_i_bin)
# [8, 9, 10]
# def を使う場合
def to_i_bin(s, base=2)
  s.to_i(base)
end

strs = %w(1000 1001 1010)
strs.map(&method(:to_i_bin))
# => [8, 9, 10]

他モジュール内メソッドを使いたい場合

こちらの記事を参考してください。

strs = %w(1000 1001 1010)
strs.map(&JSON.method(:parse))
# [1000, 1001, 1010]

# Ruby 2.7 以上
strs.map(&JSON.:parse)

カリー(Curry)化関数をmapに渡す

foo.bar()bar(foo)

JavaScript界隈でよく使われるトリックの一つ、callの活用みたいに、Rubyにも類似する使い方があります。

'abc'.toUpperCase(); // "ABC"

// 下記の形式に書き換えられる
''.toUpperCase.call('abc'); // "ABC"

Rubyだと

'abc'.upcase # "ABC"

# 下記の形式に書き換えられる
:upcase.to_proc.call('abc')
# => "ABC"

# callの簡略形式も可
:upcase.to_proc.('abc')
# => "ABC"
:upcase.to_proc['abc']
# => "ABC"

Curry化

RubyはPythonみたいにfunctools.partialのような関数が組み込まれていない(単純に私の不勉強であるかもしれませんが)ですが、Method#curryProc#curryを活用すれば、想定の効果が得られます。

to_i_c = :to_i.to_proc.curry(2)

to_i_c.call('1001').call(2)
# => 9

# Or
to_i_c['1001'][2]
# => 9

Curry化メソッドの代入

上記Curry化メソッドをmapに代入してみます

to_i_c = :to_i.to_proc.curry(2)

[2, 8, 10].map(&to_i_c['1001'])
# => [9, 513, 1001]

なぜそうなっているかは、mapでカリー化メソッドを呼び出しつづ、レシーバーにする時、最後の引数だけがiterationの対象となっていて、つまり上記のロジックを展開するとこうなります。

[2, 8, 10].map do |i|
  to_i_c['1001'][i]
end

もし、中間の引数をiterationの対象にしたい場合、引数の位置を変更する必要があります。

引数の位置を変更

to_i_r = -> (b, s) { :to_i.to_proc[s, b] }
to_i_c = to_i_r.curry(2)

strs = %w(1000 1001 1010)
strs.map(&to_i_c[2])
# => [8, 9, 10]

引数をMixinして実行(2019-10-11 Update)

とある親切なRubyのコミッターの方が教えてくれたやり方のアレンジです。

curry_exec = -> (f, *args_outer) {
  -> (*args_inner) { f[*args_inner, *args_outer] }
}.curry

%w(1000 1001 1010).map(&curry_exec[:to_i.to_proc, 2])
# => [8, 9, 10]

結局、特別な必要がなければ、大人しくProcかBlockを書くほうが、シンプルですね。

strs = %w(1000 1001 1010)

strs.map{ |s| s.to_i(2) }
# Or
strs.map(&->(s){ s.to_i(2) })

参考