株式会社コードリック株式会社コードリック
トップ
TOP
会社情報
COMPANY
請負開発
ORDER
自社開発
PRODUCT
開発実績
RECORD
お知らせ
NEWS
ブログ
BLOG
お問い合わせ
CONTACT
TOPトップCOMPANY会社情報ORDER請負開発PRODUCT自社サービスRECORD開発実績NEWSお知らせBLOGブログCONTACTお問い合わせ
BLOG
ブログ
  1. TOP > 
  2. BLOG
  3.  > プログラミング
  4.  > TypeScriptのアロー関数のちょっとした小技紹介(ポイントフリースタイル)とPrettier Pluginの作成
2023/9/8

TypeScriptのアロー関数のちょっとした小技紹介(ポイントフリースタイル)とPrettier Pluginの作成

プログラミング
みなさん、こんにちは!
株式会社コードリックの足立です。

今回はTypeScriptの関数にまつわる小技を紹介したいと思います。
アロー関数は、匿名関数を手っ取り早く作るための記法で、以下のようなコードで書けます。

const stringify = n => n.toLocaleString()
stringify(1000) // 1,000

記法が短いだけでなくthisの扱いなども変わりますが、今は置いておきましょう。

ポイントフリースタイルとは


ということでここから小技紹介コーナーです。
今作ったstringify関数を高階関数に渡すことを考えます。
きっとこんな感じになるはずです。


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を再帰的に探索して、適切に結果を出力します。
ここでは、アロー関数単体の場合とメソッド呼び出し式の中のアロー関数をターゲットにしています。
式が登場する構文要素すべてで実装ができれば完成になります!!大変すぎです!!
 
たぶん元のPrettierに部分的に追加するみたいなこともできるはずです。
https://prettier.io/docs/en/plugins.html#optional-embed

ポイントフリースタイルの罠


せっかく覚えたのでなんでもポイントフリーにしたい気分です。
でもちょっと待ってください!実は罠があります。

['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などを読んでいても度々目にします。
知識としてはぜひ身につける価値のあるものだと思います。

それでは、次の記事でお会いしましょう。

back
トップ
TOP
会社情報
COMPANY
請負開発
ORDER
自社サービス
PRODUCT
開発実績
RECORD
お知らせ
NEWS
ブログ
BLOG
お問い合わせ
CONTACT
トップ
TOP
会社情報
COMPANY
請負開発
ORDER
自社サービス
PRODUCT
開発実績
RECORD
お知らせ
NEWS
ブログ
BLOG
お問い合わせ
CONTACT
株式会社コードリック
〒920-0362 石川県金沢市古府3丁目45-2
TEL 076-249-8388 / FAX 076-203-0044
SDGsのロゴ
株式会社コードリック
〒920-0362 石川県金沢市古府3丁目45-2
TEL 076-249-8388 / FAX 076-203-0044
プライバシーポリシー
SDGsのロゴ
©株式会社コードリック. All Rights Reserved.プライバシーポリシー
©株式会社コードリック. All Rights Reserved.