かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TIL: 冪等性、参照透過性、純粋関数 についてのメモ

勉強会で 読みやすいコードのガイドライン をやっていて冪等であるのがよい。と書かれていたのですが冪等についてふわふわした感覚だったので調べたメモ。
⚠ しっかり調べた訳ではないので解釈が間違っている可能性があります :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 を実行する側が内部や状態を意識しなくて住むのであれば冪等性がある

下記は 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. 可変なデータは参照透過性を破壊する

JavaScriptarray.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.

関数の参照透過性

同じ引数で何度実行されても同じ値を返す

純粋関数 Pure functional

  • 参照透過性が担保された関数
    • 同じ引数で何度実行されても同じ値を返す
  • 副作用がない
    • 変数の値は最初に定義した値と常に同じ
    • 代入を用いないので関数外の変数を変更することもない

下記の関数は引数が同じなら何度実行されても同じ値を返すので参照透過性のある純粋関数

function succ(x: number) {
  return x + 1;
}

参照透過性が成り立つ言語は式の値がプログラムのテキストから定まるという特徴から宣言型言語 (Declarative language) と呼ばれたり、関数の数学的性質が保たれるという特徴から純粋関数型言語 (Pure functional language) と呼ばれたりする。
一方変数の値の変更が認められているような参照透過的でない言語を手続き型言語と呼ぶ。
cf. 参照透過性 - Wikipedia


[参考]

ぼっち・ざ・ろっく 面白い