Nim初心者が書くNim初心者のためのNimの高階関数について


この記事で書くこと

この記事では,Nimで提供されている高階関数(higher-order function)を紹介し,Pythonと比較します.Nimの書き方のTips的なのも含まれます.

Nimってなに?

効率的、表現力豊か、エレガント
Nimは、静的に型指定されたコンパイル済みシステムプログラミング言語です。 Python、Ada、Modulaなどの成熟した言語の成功した概念を組み合わせています。

(引用:https://nim-lang.org/ with Google翻訳)
要はPythonのような構文で,C言語なみに速いプログラミング言語です.普段Pythonを書きながらも速度に不満を持っている僕には夢のような言語であるわけです.

高階関数ってなに?

高階関数とは,関数を引数に取る関数のことです.
古くはLISPやML系の言語で用いられていましたが,近年の関数型言語の隆盛によって,多くのプログラミング言語にも実装されてきています.例えば,Pythonなどのスクリプト言語や,最近ではJavaScript(ECMAScript 2015から)にも実装されています.RustやGoでも書けるらしいけどあんま詳しくないので割愛.

Nimの高階関数

Nimのsequtilモジュールで実装されている高階関数とそのテンプレートを紹介します.

map

map
proc map[T, S](s: openArray[T]; op: proc (x: T): S {...}): seq[S] {...}

mapopenArray型の配列sとプロシージャopを引数に持ち,sの各要素にopを適用したseqを返します.openArrayとは配列を示す汎用的な型で,sequencesstringarrayが含まれます.

mapの例
import sequtils

let # これはimmutableな変数宣言
  s = [1,2,3,4] # これはArray型
  op = proc (x: int): int = x * x # 無名関数,引数の2乗を返す関数

echo s.map(op) # => @[1, 4, 9, 16] ## sの各要素が2乗されている
echo s # => [1, 2, 3, 4] ## 元の配列は変わらない
echo s.map(op) == map(s, op) # => true
## NimのTips(プロシージャの呼び方)
## p(x)とx.p() は等価

apply

apply
proc apply[T](s: var openArray[T]; op: proc (x: T): T {...}) {...}

applyopenArray型の配列sとプロシージャopを引数に持ち,sの各要素にopを適用します.mapと似ていますが,applyは戻り値をもたず,sを直接変更します.じつはもう一つapplyがありますが省略

applyの例
import sugar # 実験段階のモジュール
var # mutableな変数宣言
  x = [true, false, false, true, true]
echo x #=> [true, false, false, true, true]

apply(x, b => not b) # xの各要素のnotをとる
echo x #=> [false, true, true, false, false]
## NimのTips(無名関数の書き方)
## b => not bは無名関数の省略記法.左辺が引数,右辺が戻り値
## ただしimport sugarが必要

filter

filter
proc filter[T](s: openArray[T]; pred: proc (x: T): bool {...}): seq[T] {...}

filteropenArray型の配列sとプロシージャpredを引数に持ち,predtrueを返すsの要素で構成されるseqを返します.

filterの例
import strutils

let
  str = "happY holidAYs"
  upperStr = str.filter(s => s.isUpperAscii()) # 大文字をフィルタ

echo str #=> "happY holidAYs"
echo upperStr #=> @['Y', 'A', 'Y']
echo upperStr.join("") #=> "YAY" ## string型の配列の要素を結合 (Pythonと同じ)
## NimのTips(命名)
## Nimの変数,プロシージャはcamelCaseでの宣言が推奨されています.

keepIf

keepIf
proc keepIf[T](s: var seq[T]; pred: proc (x: T): bool {...}) {...}

keepIfseq型のsとプロシージャpredを引数に持ち,predtrueを返す要素のみをsに残します.ただし,引数がopenArrayでなくseqであることに注意.

便利なテンプレート(mapIt, applyIt, filterIt, keepItIf)

これらはそれぞれmap, apply, filter, keepIfのテンプレートです.これらを使えば(少しだけ)簡単に高階関数を呼び出せます.使い方はどれもほとんど同じなので,mapItの例を紹介します.

mapItの例
let 
  v = [1,2,3,4,5]
  w = v.mapIt(it*2) # itはvの各要素,これは各要素を2倍にするという意味
echo w # => @[2, 4, 6, 8, 10]

ポイント
- mapIt(その他も同様)の引数は配列と
- テンプレート内のitは引数の配列の各要素
- 戻り値はもとの関数と同様にseq

Pythonとの比較

Pythonにもmap, filter, reduce (foldl, foldrに当たる) が実装されていますが,applyやkeepIfに相当するものはありません(pandasにはある).そのため,リストlmapをかけた後のリストを使いたい場合は,

l = [1, 2, 3, 4] # 各値を二倍したい
mapped_l = map(lambda x: x*2, l) # mapped_lはlistではない
assert type(mapped_l) is map # => true
mapped_l = list(mapped_l) # listに戻す

としなければなりません.mapの戻り値の型はmap objectで引数の方とは異なります.これはジェネレータなので,元の型として使いたい場合は明示的に戻さなければなりません.これはfilterでも同様でfilterの戻り値はfilter objectです.例えば,

l = [-3, -2, -1, 0, 1, 2, 3] 
p = filter(lambda x: x>0, l) # 正の要素だけ残す
len(p) # pはfilter objectなのでエラー
len(list(p)) # これは大丈夫.答えは3
len([li for li in l if li > 0]) # これも大丈夫.こっちのほうがPythonらしい書き方???

これが個人的にあまり好きではありません.Nimの場合はl.map(proc ...)の戻り値の型はseqなので,このような処理をする必要がないため使いやすいです(個人的な感想).

おわりに

Nimの高階関数についてまとめてみました.Nimはまだまだ発展途上の言語ですが,これからのさらなる発展を願っています.すこしでも皆様の参考になれば幸いです