勉強会で 読みやすいコードのガイドライン をやっていて冪等であるのがよい。と書かれていたのですが冪等についてふわふわした感覚だったので調べたメモ。
⚠ しっかり調べた訳ではないので解釈が間違っている可能性があります :pray:
冪等性 idempotency, idempotence
- 同じ操作を何度繰り返しても、同じ結果が得られるという性質
f(x) = f(f(x))
が成立する (数学的な厳密な冪等性?)abs(-100) = abs(abs(-100))
なのでabs()
関数は冪等性があるsqrt(16) === sqrt(sqrt(16))
は成立しないのでsqrt()
関数は冪等性がないMath.random()
は実行する度に得られる結果が異なるので冪等性はない- 圧縮する zip を
zip(zip(x))
とすると archive が二重になるのでzip(x) = zip(zip(x))
は成立しないからzip()
は冪等性がない
- 何回呼び出しても同じ結果になる
import 'react'
は何回呼び出しても同じなのでimport
は冪等性があるtsc
でのコンパイルはコードと設定が同じなら何回実行しても同じ結果になるのでtsc
は冪等性があるbuttom.toggle()
のような関数は実行のたびに対象の状態がtrue
又はfalse
と変化するので冪等性はない- ※
toggle()
関数が状態に関わらず常にvoid
undefined
を返すのなら外からみて冪等性があるのだと思うが、対象の状態が副作用的に変化して扱う側はそれを覚えておかなければならないので冪等性が無いとして扱ったほうが良さそう
- 冪等性は外からみた性質で、内部実装に違いが合っても良い
下記は API を使う側からすれば「同じ操作を何度繰り返しても、同じ結果が得られるという性質」があるので冪等性がある
// 常にアップデートする API関数 function updateEmail(user, newEmail) { sql.exec("UPDATE users SET email = {newEmail} WHERE id = {user.id}"); return; } // 内部で処理が異なるが常に同じ結果を返す API 関数 function updateEmail(user, newEmail) { if (user.email == newEmail) { // 更新はしない } else { sql.exec("UPDATE users SET email = {newEmail} WHERE id = {user.id}") } return; }
cf.
参照透過性 Referential transparency
- プログラムの構成要素が同じ者同士は等しい ->
A === A
2 === 2
は等しいx = 1
の時x === x
は等しい
- 変数の値は最初に定義した値と常に同じ ->
var
,let
でなくconst
- 変数に値を割り当てなおす演算である代入 (Assignment) を行う式は存在しない
変数などがどこに記録されている値を参照しているのかを考慮する必要がない = 参照透過性がある
abs(-100)
は常に100
で副作用も持たないのでabs()
関数には参照透過性がある (冪等性もある)sqrt(16)
は常に4
で副作用も持たないのでsqrt()
関数には参照透過性がある (冪等性は無い)
1. 可変なデータは参照透過性を破壊する
JavaScript の array.push()
のような破壊的メソッドは参照している値を変化させるので参照透過性がない
const arr = [1]; arr.push(2); console.log(arr); // => [1, 2]; arr.push(3); console.log(arr); // => [1, 2, 3];
2. 代入は参照なので代入を用いると参照透過性が破壊される
let x = 0; add = (y: number): number => { x = x + 1; return x + y; } add(1); // => 2 add(1); // => 3
3. 副作用(side effect) は、関数外の機能・結果に影響をもたらすので参照透過性がない
React の非同期関数でデータを取得し状態を変更するのは、入力値 (props
) とは別に返却するコンポーネント (return される JSX) を変更するので副作用であり、参照透過性が無い
import { useState, useEffect, FC } from 'react'; type UserInfoProps = { userId: number; }; const UserInfo: FC<UserInfoProps> = ({ userId }) => { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/user/{userId}`) .then((res) => res.json()) .then((json) => setUser(json)); }, [userId]); return user ? <h1>Hello, {user.name}</h1> : <div>Loading…</div> }
最初にUserInfoコンポーネント(関数)は userId
を入力に受け取り実行された時は <div>Loading...</div>
返す。しかしその後で useEffect
内の非同期処理が完了した後に <h1>Hello, {user.name}</h1>
を返すので返却する値が変わってしまう
同様に head の title などコンポーネント外の DOM を書き換えるのもコンポーネント ( = 関数) の外の DOM を参照して変更しているので副作用であり、参照透過性がない
import { useEffect, FC } from 'react'; const Example: FC = () => { useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; }); return ( <div>Example Component</div> ); }
cf.
- 参照透過性 - Wikipedia
- 副作用 (プログラム) - Wikipedia
- 副作用フックの利用法 – React
- コールバックで副作用となる非同期処理|イベントループとプロミスチェーンで学ぶJavaScriptの非同期処理
- React入門チュートリアル (6) 副作用 | Hypertext Candy
関数の参照透過性
同じ引数で何度実行されても同じ値を返す
純粋関数 Pure functional
- 参照透過性が担保された関数
- 同じ引数で何度実行されても同じ値を返す
- 副作用がない
- 変数の値は最初に定義した値と常に同じ
- 代入を用いないので関数外の変数を変更することもない
下記の関数は引数が同じなら何度実行されても同じ値を返すので参照透過性のある純粋関数
function succ(x: number) { return x + 1; }
参照透過性が成り立つ言語は式の値がプログラムのテキストから定まるという特徴から宣言型言語 (Declarative language) と呼ばれたり、関数の数学的性質が保たれるという特徴から純粋関数型言語 (Pure functional language) と呼ばれたりする。
一方変数の値の変更が認められているような参照透過的でない言語を手続き型言語と呼ぶ。
cf. 参照透過性 - Wikipedia
[参考]
ぼっち・ざ・ろっく 面白い冪等性と参照透過性、いつも混乱する。冪等性は、f(x) = f(f(x)) が成立するだけ。参照透過性は、1) 入力から出力が一意に決定、2) 副作用がない を満たす性質。
— メロン食べたい (@yn2011) 2020年10月18日
なので例えば、絶対値を求める abs(x) は冪等だし参照透過でもある。