const stringify = (n: number) => n.toLocaleString()
const res = [1, 100, 1000, 1000000].map(num => stringify(num))
// res = ['1', '100', '1,000', '1,000,000']
すばらしい!
高階関数を使った記述は最近では当たり前に使われていて、
標準の組み込み関数や組み込みオブジェクトのメソッドにも多く使われています。
ところで、先ほどの関数は次のようにも書けます。
const stringify = (n: number) => n.toLocaleString()
const res = [1, 100, 1000, 1000000].map(stringify)
// res = ['1', '100', '1,000', '1,000,000']
mapの中のアロー関数が短くなりました!
イメージとしては、mapが型の合う関数リテラルを受け取っているので、
mapの中でアロー関数を使って関数リテラルを作っても、もともとある関数リテラルを使ってもいいでしょう理論です。
渡す関数はfunctionで定義した関数でも問題ありません。
このように、関数を引数なしの形で書くことを「ポイントフリースタイル」と言います。
ポイントフリースタイルの利点としては、煩わしい内部変数名(特に自明なもの)を付ける必要がないことです。
内部変数を命名するのが面倒というのは、ある程度共通した感覚のようで、
たとえば、Scalaの"_"によるプレースホルダー構文や、Kotlinの"it"、Swiftの"$0"などは、同様の目的をもった機能です。
ポイントフリースタイルへの変換
仕組みや利点は分かりました。
ただ、パッと見てポイントフリーにできるかどうかを判断するのは、ちょっと慣れが必要です。
実は、誰でも判別できる機械的な変換があるので紹介します!
先ほど挙げた例で説明します。
mapの中の関数に注目すると、引数がnumで、関数呼び出し式の最後がnumを引数にとります。
もっと端的に言うと、関数の先頭と後ろが同じです。
このとき、これらを打ち消しあうことができるのです!
(num) => stringify
(num) これならだれでもできます。
ポイントフリーにしたいのなら、先頭と末尾が一致しているときのみ、今の手順で行えばいいですし、逆にポイントフリーで書かれたコードは、先頭と末尾に同じ引数をつけて復元してあげればよいです。
Prettier Pluginを使ったポイントフリースタイルへの自動変換
ところで機械的にできることは、機械に任せたいです。
フォーマッタとしてよく使われるPrettierのプラグインとして導入しましょう。
こちらの記事を参考にしました。
Vim & VSCodeのフォーマッタを自作する(分析SQLを例に)
import { parse } from 'acorn'
import { AstPath, Doc, doc } from 'prettier'
export const languages = [
// impl
]
export const parsers = {
// impl
}
function printTS(
path: AstPath,
options: Record<string, any>,
print: (path: AstPath) => Doc
) {
// 中略
if (node.type == 'ArrowFunctionExpression') {
return path.call(printPointFree(options))
}
// アロー関数式でpoint-free-styleを適用する実装
if (node.type == 'ExpressionStatement') {
return path.call(print, 'expression')
}
// methodの関数呼び出し式の部分でpoint-free-styleを適用する実装
// (calleeの式).(メソッド呼び出し式)の両方の式にapply
if (node.type === 'CallExpression') {
return `${path.call(print, 'callee')}(${path.call(print, 'arguments')})`
}
if (node.type === 'MemberExpression') {
return `${path.call(print, 'object')}.${path.call(print, 'property')}`
}
if (node.expression) {
return path.call(print, 'expression')
}
if (node) {
return options.originalText.slice(node.start, node.end)
}
}
function printPointFree(options: Record<string, any>): (path: AstPath) => Doc {
return (path: AstPath) => {
const node = path.node
const params = node.params.map((param) => {
return param.name
})
// 関数のbodyが関数呼び出し式でないときは、point-freeは関係ない
if (node.body.type !== 'CallExpression') {
return options.originalText.slice(node.start, node.end)
}
// 以降、bodyが関数呼び出し式である
const args = node.body.arguments.map((arg) => {
return arg?.name
})
// 引数の数が同じ && identifierもその登場順も一致する
if (params.length !== args.length) {
return options.originalText.slice(node.start, node.end)
} else {
if (
params.reduce((acc, param, index) => {
return acc && param === args[index]
}, true)
) {
// point free
return node.body.callee.name
} else {
return options.originalText.slice(node.start, node.end)
}
}
}
}
export const printers = {
// impl
}
省略された部分を確認したい方は
こちらのリポジトリから確認してください。
Prettierのプラグインを作るためには、languagesとparsersとprintersをexportする必要があります。
parserでASTを生成して、printerでそのASTを走査してフォーマット出力をする感じです。
parserは好きなものを使えばよいです。
printer関数の中でASTを再帰的に探索して、適切に結果を出力します。
ここでは、アロー関数単体の場合とメソッド呼び出し式の中のアロー関数をターゲットにしています。
式が登場する構文要素すべてで実装ができれば完成になります!!大変すぎです!!
ポイントフリースタイルの罠
せっかく覚えたのでなんでもポイントフリーにしたい気分です。
でもちょっと待ってください!実は罠があります。
['1'].map(n => parseInt(n))
// [1]
['1'].map(parseInt)
// [1]
['1', '2'].map(n => parseInt(n))
// [1, 2]
['1', '2'].map(parseInt)
// [1, Nan]
mapが複数のパラメータをとり、parseIntも複数のパラメータを取れるがために、この問題が起こります。
要は、(num, radix) => parseInt(num, radix) なのか (num) => parseInt(num) なのか見分けがつかないということです。
呼び出し側のパラメータの数と、引数の関数のパラメータの数が一意に決まっていて一致するときだけで…みたいなことも考えましたが、今度は型シグネチャが必要です。
https://ts-ast-viewer.com/ を見て、型シグネチャが取れているので真似しようと奮闘した話もありますが、またの機会にします。
まとめ
ポイントフリースタイルとは何かということと、簡単な変換方法をここまでご紹介しました。
内部変数を宣言する必要がない利点はあるものの、可読性が必ずしも上がるとは限らず、
さらにコードの意味を変えてしまうことがあるため、注意しながら使いましょう。
コードリーディングにおいては、OSSなどを読んでいても度々目にします。
知識としてはぜひ身につける価値のあるものだと思います。
それでは、次の記事でお会いしましょう。