無駄と文化

実用的ブログ

指定したキーだけを省略可能にするユーティリティタイプを書く

TypeScript は便利だ。型検査で値が保証されるのはとても頼もしい。
とはいえ場合によっては型検査を通すために不必要にタイプ量が増えてしまうことがある。

例えば下記のような型が 外部ライブラリによって生成される としよう。

type User = {
    __typename: 'user';
    id: string;
    name: string;
    gender: 'male' | 'female';
    department: {
        __typename: 'department';
        id: string;
        name: string;
    };
};

いま User 型の値を生成して使いたい。ただし、とても重要な前提条件として キー __typename には関心がない とする。

const user: User = {
    __typename: 'user',  // このキーは使うつもりがないし関心もない
    id: 'u001',
    name: 'SUZUKI Ichiro',
    gender: 'male',
    department: {
        __typename: 'department',  // このキーは使うつもりがないし関心もない
        id: 'd001',
        name: 'sales',
    }
};

このような値を定義すれば型検査をパスする。 が、使うつもりのない __typename をわざわざ書くのは面倒だと思った、そう仮定しよう。

 

特定のキーを省略可能にしたい

type User においてキー __typename は必須だった。もしも何らかの方法で以下のような型 BetterUser を生成できたら。

type BetterUser = {
    __typename?: 'user';
    id: string;
    name: string;
    gender: 'male' | 'female';
    department: {
        __typename?: 'department';
        id: string;
        name: string;
    };
};

__typename?: となっているのに注目してもらいたい。キー __typename は省略可能なので以下のような値でも受け入れられる。

const user: BetterUser = {
    // __typename を書かなくて済む
    id: 'u001',
    name: 'SUZUKI Ichiro',
    gender: 'male',
    department: {
        // __typename を書かなくて済む
        id: 'd001',
        name: 'sales',
    }
};

いかにも良さそうだ。__typename を省略可能にするにはどうすればいいだろうか。

 

ユーティリティタイプでどうにかする

既存の型をもとに別の型を生成する 型レベル関数 のようなものを TypeScript においては「ユーティリティタイプ」と呼んでいる。1

例えば、全てのキーを省略可能するユーティリティタイプ Partial<T> は次のように書ける。

type Partial<T> = { [K in keyof T]?: T[K] };

動作イメージはこうだ、

type User = {
    __typename: 'user';
    id: string;
    name: string;
    gender: 'male' | 'female';
};

type PartialUser = Partial<User>;
// => type PartialUser = {
//     __typename?: 'user' | undefined;
//     id?: string | undefined;
//     name?: string | undefined;
//     gender?: 'male' | 'female' | undefined;
// };

実はこのようなユーティリティタイプ Partial<T> は TypeScript に最初から用意されている。そのためわざわざ定義しなくても使える。とても便利だ。

だが今回は 指定したキーだけ 省略可能にしたいのだった。全てのキーを省略可能する Partial<T> は少しやりすぎだ。

 

Partial<T> をそのまま使うことは出来なそうだが「指定したキーだけ省略可能にするユーティリティタイプを定義して使う」という方針は決まった。
先に名前を決めてしまおう SelectivePartial というユーティリティタイプが欲しい。

type SelectivePartial<T, K> = {};  // TODO: 実装する

type BetterUser = SelectivePartial<User, '__typename'>;

このように書けると良さそうだ。

 

Partial を応用した単純な例

指定したキーだけ省略可能にするのはわりと簡単で、ChatGPT に頼むとサラッと書いてくれる。

type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

このように。

Omit<T, K>Pick<T, K> も TypeScript に標準のユーティリティタイプだ。Omit<T, K>K で指定したキーを T から除外してくれる。Pick<T, K> はその逆で、K で指定したキーのみ T から残してくれる。

type User = {
    __typename: 'user';
    id: string;
    name: string;
    gender: 'male' | 'female';
};

type Omitted = Omit<User, '__typename'>;  // User から __typename を除外
// => type Omitted = {
//     id: string;
//     name: string;
//     gender: 'male' | 'female';
// };

type Picked = Pick<User, '__typename'>;  // User から __typename だけを残す
// => type Picked = {
//     __typename: 'user';
// };

これと先ほどの Partial<T> を組み合わせて、さらに & でつないで交差型 (Intersection Types) を作ると「指定したキーだけ省略可能にする」が実現できる。

type Parted = Partial<Pick<User, 'name'>>;  // name だけを残し、省略可能にする
// => type Parted = {
//     __typename?: 'user' | undefined;
// };

type BetterUser = Omitted & Parted;
// => type BetterUser = {
//     __typename?: 'user' | undefined;
//     id: string;
//     name: string;
//     gender: 'male' | 'female';
// };

一連の処理をまとめて型変数を使って書くと最初に示した SelectivePartial<T, K> の定義になる。

type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type BetterUser = SelectivePartial<User, '__typename'>;
// => type BetterUser = {
//     __typename?: 'user' | undefined;
//     id: string;
//     name: string;
//     gender: 'male' | 'female';
// };

最低限の要件を満たしてはいるが不満が残る。この方法ではネストしたオブジェクト型に再帰的に作用させることはできない。

 

SelectivePartial を再帰的に作用させたい

最初の例をもう一度見てみよう。最初に示した User 型はこんな感じだった。

type User = {
    __typename: 'user';
    id: string;
    name: string;
    gender: 'male' | 'female';
    department: {
        __typename: 'department';
        id: string;
        name: string;
    };
};

SelectivePartial<T, K>User に作用させると次のようになる。

type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type BetterUser = SelectivePartial<User, '__typename'>;
// => type BetterUser = {
//     __typename?: 'user' | undefined;
//     id: string;
//     name: string;
//     gender: 'male' | 'female';
//     department: {
//          __typename: 'department';
//          id: string;
//          name: string;
//     };
// };

department の中の __typename はあいかわらず省略できない。

 

ユーティリティタイプを再起的に作用させるには、関数の再帰呼び出しのアイデアを借用すればいい。と、その前に Omit<T, K>Partial<Pick<T, K>> を自前の実装で書き直す必要がある。
Omit<T, K>Pick<T, K> とても似たコードで実装できる。

type Omit<T, K> = {
    [L in keyof T as L extends K ? never : L]: T[L];
};

type Pick<T, K> = {
    [L in keyof T as L extends K ? L : never]: T[L];
};

どちらの実装も L in keyof T の部分で T の各キーに対して作用を起こしている。
Omit の方では L extends K ? never : L で「キー LK に含まれていればキー L を消し去る」という作用を表現している。Pick はその逆で、L extends K ? L : never で「キー LK に含まれていなければキー L を消し去る」という作用を表現している。

Pick<T, K> の自前実装をもとに Partial<Pick<T, K>> も自前で書き下したい。じつはこれはとても簡単で、キーの後ろに ? を打って省略可能であることを表明するだけだ。

type PartialPick<T, K> = {
    [L in keyof T as L extends K ? L : never]?: T[L];
};

ここまでのコードをまとめて前の節で書いた再帰的でない SelectivePartial<T, K> を書き直してみよう。

// type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type SelectivePartial<T, K> = {
    [L in keyof T as L extends K ? never : L]: T[L];
} & {
    [L in keyof T as L extends K ? L : never]?: T[L];
};

ここまでくればユーティリティタイプを "再帰呼び出し" するのは簡単だ。T[L] の部分をちょっといじってあげればよい。

type SelectivePartial<T, K> = {
    [L in keyof T as L extends K ? never : L]: SelectivePartial<T[L], K>;
} & {
    [L in keyof T as L extends K ? L : never]?: SelectivePartial<T[L], K>;
};

改良版の SelectivePartial<T, K> を使ってみよう。

type User = {
    __typename: 'user';
    id: string;
    name: string;
    gender: 'male' | 'female';
    department: {
        __typename: 'department';
        id: string;
        name: string;
    };
};

type BetterUser = SelectivePartial<User, '__typename'>;
// => type BetterUser = {
//     __typename?: 'user' | undefined;
//     id: string;
//     name: string;
//     gender: 'male' | 'female';
//     department: {
//         __typename?: 'department' | undefined;
//         id: string;
//         name: string;
//     };
// };

かなり良くなった。

 

配列型にも適用したい

もしも最初に手元にあるのが User 型ではなく Users 型だったらどうなるだろうか。

type Users = {
    __typename: 'user';
    id: string;
    name: string;
    gender: 'male' | 'female';
    department: {
        __typename: 'department';
        id: string;
        name: string;
    };
}[];

先ほどとよく似ているが [] がついている。「オブジェクト型の配列」型だ。先ほど定義した SelectivePartial<T, K> を使って Users から BetterUsers を作れるだろうか。

 

TypeScript の型についてのちょっとしたテクニックを知っていれば T[]T を相互変換できる。

type A = T[];
type B = A[number];
//   ^? T

type C = T;
type D = C[];
//   ^? T[]

T[] 型の A に対して A[number] とすると T 型が得られる。慣れないと奇妙な感じがするが、イデオムとして覚えてしまうと便利だ。
C = T に対して C[] と書くと T[] が得られるのは、まぁ、自明だろう。

 

このテクニックを使うと SelectivePartial<T, K>Users に適用できるようになる。

type Users = {
    __typename: 'user';
    id: string;
    name: string;
    gender: 'male' | 'female';
    department: {
        __typename: 'department';
        id: string;
        name: string;
    };
}[];

type User = Users[number];

type BetterUser = SelectivePartial<User, '__typename'>;

type BetterUsers = BetterUser[];
// => type BetterUsers = {
//     __typename?: 'user' | undefined;
//     id: string;
//     name: string;
//     gender: 'male' | 'female';
//     department: {
//         __typename?: 'department' | undefined;
//         id: string;
//         name: string;
//     };
// }[];

もちろんひとまとめにして次のように書いても同じ意味になる。

type BetterUsers = SelectivePartial<Users[number], '__typename'>[];

 

まとめ

Omit<T, K> & Partial<Pick<T, K>> というテクニックは ChatGPT がすぐに教えてくれた。それを再帰的に適用するにはいくつか発想の転換が必要だった。

いまはスッキリ理解できていても TypeScript を書かずにいる期間が空くとすぐに筋力が衰えてしまう。少しでも忘却に抗うためにここに書き記す。

 

 

私からは以上です。

 

おまけ

今回書いたコードがここにある、


  1. TypeScript にはさまざまな 組み込みのユーティリティタイプ がある、知っておくといつか何かの役に立つかもしれない

毎年3日間だけ花粉症に罹っている【定点観測】

ここ数年、春先に鼻炎になる。「花粉症か」「自分にもついに来たか」と思って薬を飲むのだが3日ほどすると治っている。そんなことが続くので冗談めかして「毎年3日間だけ花粉症になるんです」などと言っていた。

 

2024年、今年も花粉症の季節がやってきた。

そろそろ実態を解き明かしたい気持ちがあるので、定点観測1年目という気持ちでブログに書き残そうと思う。

 

◾️2024-02-25 sun

朝からやたらと目が痒かった。数週間前に結膜炎に罹っていたので、それが再発したか?と思いながら薬局で薬用の目薬を買って挿した。13時頃。

 

車で30分ほど走って実家に顔を出した。この時点でくしゃみが連続で出るようになっていた。ムズムズが止まらない。ただこの時点では風邪の初期症状のような気もしたし、実家でひさしぶりににペットの犬と触れ合ったことによるアレルギーのような気もした。

 

夜、入浴したら少しだけ落ち着いた。とはいえくしゃみは出る、鼻水も出る。鼻詰まりで顔がポッと火照る感じがする。何らかの鼻炎だなと思い薬局で鼻炎薬を買って飲んだ。21時頃。

 

深夜、喉が痛みだした。唾を飲み込むときに喉にゴロゴロとした違和感と痛みがあり、よく眠れなかった。

 

◾️2024-02-26 mon

朝から調子が悪い。目の痒み、鼻水、喉の痛みが相変わらずで、これは花粉症というやつでは?とかなり確信を得た。もともとハウスダストのアレルギー持ちでくしゃみが止まらなくなることがあったが、目や喉まで同時にやられるのは初めての経験だった。さらに言うと頭がボーっとして集中できない、深い思考が要求されるようなタスクができないなど、かなり苦痛だし仕事にも支障が出ていた。

再び薬局に行きアレルギ製の鼻炎薬を買って飲んだ。15時頃。

 

夜、服薬のおかげで喉の痛みは和らいだが、頭がふらつく感覚があった。昨晩は喉の痛みで寝不足だったのでそのせいかと思いながら就寝。

 

◾️2024-02-27 tue

朝からだいぶ弱っていた。元気が出ない、気力が湧かない、鼻が詰まる、頭がふらつく。ああこんな状態が数週間続くのかと軽く絶望しながら仕事を開始した。

相変わらず思考を要するタスクは思うように進まない。だんだんと眩暈がしてきて生唾が出るようになってきた。12時頃。

(私にとって生唾が出るのは吐き気を伴う体調不良の前兆だった)

 

昼休みを長めにもらってベッドに横になった。何となく寒気がする気がして羽毛布団に毛布を重ねて潜り込んだ。しっかりと暖かいセットアップのはずなのに横になっていて寒さを感じる。悪寒がするというやつで、花粉症ではなく風邪かもっとひどいウイルス性の何かを疑い始めた。15時頃。

 

横になって少しは症状が和らいだ気がしたので、起き上がって本日中に最低限終わらせておきたいタスクだけ消化することにした。終わったのは18時頃。

 

早めに退勤して夕食を済ませて、私用の Web 会議に参加して、風呂に入った。不思議なことに体調がかなり回復しているのを感じた。目の痒みはおさまり、鼻は通っている、喉の痛みもほぼない。ふらつきや吐き気もほぼ意識しなくてよくなっていた。22時頃。

もしかしてアレルギー性の鼻炎薬が体調に合わずにフラフラになっていたのか?と疑って、その日の晩の薬は飲まずに寝た。

 

◾️2025-02-28

朝起きる。完全回復という感じではないが、昨日に比べるとだいぶ体調が良いのを実感する。何よりも頭が働く。ちゃんとした思考ができる。13時頃。

 

結局、薬を飲まないままくしゃみも喉の痛みも治ってしまった。2024年度の私の花粉症さ治った。

 

 

いったいこれは何なんだ?経過を見て判断しようと思う。

 

型無しラムダ計算学習用ステップ評価器 skiMogul を作っている話

みなさま ラムダ計算 をご存知でしょうか。
ラムダ計算はある種の関数型プログラミング言語の体系で、変数と関数, そして関数適用というミニマルな構成要素だけでチューリング完全な表現力を持っています。

この記事では、ラムダ計算の中でも特に単純な 型無しラムダ計算 に着想を得てラムダ計算のステップ評価器を作っている話について書きます。

 

既存のモデルへの不満

型無しラムダ計算の処理系は昔からいくつも実装されていて、有名なところで UnlambdaLazy_K などがあります。
どちらも SKI コンビネーター理論 に基づいていて、 s, k, i という たった3つの組み込み関数でプログラムを記述することが可能 1 です。

かく言う私も Unlambda から型無しラムダ計算に入門したクチです。しかし、従来の処理系には長らく不満がありました。それは関数の計算過程が印字不可で、計算の過程が非常に見えづらいことです。
早い話 Unlambda や Lazy_K は print デバッグできないのがツラい という事なのですが。私の見立てではこの print デバッグで試行錯誤できない感が ラムダ計算の世界から入門者を遠ざけている要因になっている ように思います。

 

ラムダ計算の計算過程を可視化する

そこで、『無いものは作ればいい精神』です。型無しラムダ計算学習用ステップ評価器 skiMogul を作りました。
実際の動作を見てもらう方がイメージしやすいと思うので GIF を、

型無しラムダ計算学習用ステップ評価器 skiMogul

mogul-lang.mudatobunka.org

ラムダ式をステップ評価する様が見て取れると思います。
今のところ

  • 論理演算 (NOT, AND, OR, XOR)
  • 整数(チャーチ数)の操作 (後者関数 SUCC, 前者関数 PRED)
  • 整数(チャーチ数)の演算 (和 ADD, 差 SUB, 積 MUL, 商と剰余 DIV)
  • 整数(チャーチ数)の同士の比較 (ゼロ比較 IS_ZERO, 等値比較 EQ, 不等比較 GT, LT)
  • 連結リスト操作 (CONS, CAR, CDR)
  • 無名再帰 (Y コンビネータ)
  • 自作関数の定義

などが出来ます。

 

シンタックス

skiMogul の記法について簡単に解説してみます。

関数適用 (関数実行)

まず、 i という組み込みの関数があります。 Identity の略で、引数をそのまま返す関数です。

> i
i

このように、i 単体を評価しても何も起きません。

では、関数 i に引数 x を与えて実行してみましょう。JavaScript 風にパーレン () で関数呼び出しを書きます。

> i(x)
i(x)
⇒ x

skiMogulで実行する

はい、実行結果は x です。
このように関数に引数を与えて実行することを 関数適用 と云います。この場合 ix を適用しました。

 

他にも組み込み関数を使ってみましょう。定数関数 k は2つの引数を取り、2つめの引数を捨てて1つめの引数をそのまま返します。
k は Constant (Konstant) の略ですね、たぶん。

k(x, y)
⇒ x

skiMogulで実行する

「2つの引数を取る」と書きましたが、これは正確な表現ではありません。厳密には Mogul の世界には多変数関数が存在せず、1引数関数しか扱えないからです。
k(x, y)k(x)(y) と同じ意味に解釈されます。これは kx を適用した結果に y を適用していると読めます。このように1変数関数を用いて多変数の関数適用を表現する方法を「関数のカリー化」と言います 🍛

 

もう一つ欠かせない関数 s を使ってみましょう。
sSubstitution の略で、3引数 x, y, z を与えると x(z, y(z)) を返します。

> s(x, y, z)
s(x, y, z)
⇒ x(z, y(z))

skiMogulで実行する

関数合成引数の入れ替え を実現するために重要な関数です。

関数抽象と関数定義

ラムダ計算に欠かせないのが 関数抽象 です。
モダンなプログラミング言語に親しんだ方であれば 無名関数 または クロージャー (closure) の事だと云えば伝わりやすいでしょうか。

例として、引数 x に対して x(x) を返す関数抽象を以下のように書きます。

x => x(x)

関数抽象に引数を与えて評価することができます。 a を適用してみましょう。

> (x => x(x))(a)
(x => x(x))(a)
⇒ a(a)

skiMogulで実行する

いいですね。

関数定義

関数抽象は 別の変数に代入する こともできます。

> FOO = x => x(x)
> FOO(a)
FOO(a)
⇒ (x => x(x))(a)
⇒ a(a)

skiMogulで実行する

このようにして自作関数 FOO を定義することができました。これが 関数定義 です。

多変数関数

多変数関数は関数抽象をネストさせることで実現します。

> x => y => y(x)
x => y => y(x)

> (x => y => y(x))(a)(b)
(x => y => y(x))(a)(b)
⇒ (y => y(a))(b)
⇒ b(a)

skiMogulで実行する

上手く動いていますね。
このように1引数の関数抽象をネストさせて多変数関数を表現する方法を カリー化 と云います。

ちなみに、skiMogul では多変数風の関数抽象の糖衣構文を用意しているので下記のように書くこともできます。

> (x, y) => y(x)
(x, y) => y(x)

> ((x, y) => y(x))(a, b)
((x, y) => y(x))(a, b)
⇒ (y => y(a))(b)
⇒ b(a)

関数の定義を参照する

ここまでに見てきたように、 Mogul では関数適用を変数に代入することで自作の関数を定義することが出来ます。
逆に定義済みの関数の定義を参照するには ? コマンド を使います。

> ? s
s(x, y, z) = x(z, y(z))

skiMogulで実行する

Mogul には他にも多くの組み込み関数が定義されています。 Context パネルで定義済みの関数を一覧できます。
また ? コマンドを単体で実行することでも同じように見ることができます。

> ?
i(x) = x
k(x, y) = x
s(x, y, z) = x(z, y(z))
Y(f) = (x => f(x(x)))(x => f(x(x)))
...
XOR(x, y) = x(NOT(y), y)
ι(f) = f((x, y, z) => x(z, y(z)), (x, y) => x)

skiMogulで実行する

skiMogul で計算っぽいことをやってみる

遊んでみましょう。

論理演算

論理演算のために必要な一通りの関数が用意されています。
TRUE, FALSE, NOT, AND, OR, XOR などです。

> AND(TRUE, FALSE)
AND(TRUE, FALSE)
⇒ TRUE(FALSE, FALSE)
⇒ ((x, y) => x)(FALSE)(FALSE)
⇒ (y => FALSE)(FALSE)
⇒ FALSE

> AND(TRUE, NOT(FALSE))
AND(TRUE, NOT(FALSE))
⇒ TRUE(NOT(FALSE), FALSE)
⇒ ((x, y) => x)(NOT(FALSE))(FALSE)
⇒ (y => NOT(FALSE))(FALSE)
⇒ NOT(FALSE)
⇒ FALSE(FALSE, TRUE)
⇒ ((x, y) => y)(FALSE)(TRUE)
⇒ (y => y)(TRUE)
⇒ TRUE

> AND(OR(FALSE, TRUE), NOT(FALSE))
AND(OR(FALSE, TRUE), NOT(FALSE))
⇒ OR(FALSE, TRUE)(NOT(FALSE), FALSE)
⇒ FALSE(TRUE, TRUE)(NOT(FALSE), FALSE)
⇒ ((x, y) => y)(TRUE)(TRUE, NOT(FALSE), FALSE)
⇒ (y => y)(TRUE)(NOT(FALSE), FALSE)
⇒ TRUE(NOT(FALSE), FALSE)
⇒ ((x, y) => x)(NOT(FALSE))(FALSE)
⇒ (y => NOT(FALSE))(FALSE)
⇒ NOT(FALSE)
⇒ FALSE(FALSE, TRUE)
⇒ ((x, y) => y)(FALSE)(TRUE)
⇒ (y => y)(TRUE)
⇒ TRUE

skiMogulで実行する

IF を使うと if式 っぽいものを書くこともできます。

> IF(FALSE)(x, y)
IF(FALSE, x, y)
⇒ FALSE(x, y)
⇒ ((x, y) => y)(x)(y)
⇒ (y => y)(y)
⇒ y

整数演算

020 までの整数 2 は定義済みです。
ADD, 差 SUB, 積 MUL, 商と剰余 DIV を計算することもできます。

> ADD(2, 3)
ADD(2, 3)
⇒ (f, x) => 2(f, 3(f, x))

この評価結果はラムダ計算的に正しいのですが、最終結果を見ても釈然としないかも知れませんね。
等価比較 EQ を使って ADD(2, 3)5 に等しいことを確かめてみましょう。

> EQ(ADD(2, 3), 5)
EQ(ADD(2, 3), 5)
⇒ AND(GTE(ADD(2, 3), 5), LTE(ADD(2, 3), 5))
⇒ GTE(ADD(2, 3), 5)(LTE(ADD(2, 3), 5), FALSE)
⇒ IS_ZERO(SUB(5, ADD(2, 3)))(LTE(ADD(2, 3), 5), FALSE)
...
⇒ (u => TRUE)(_ => FALSE)
⇒ TRUE

skiMogulで実行する

結果は TRUE、つまり 2+3 は 5 に等しいということですね。

 

実装

skiMogul は Rust で実装されています。
Rust コードを wasm にコンパイルして GitHub Pages にデプロイしています。計算のためにサーバーと通信することはありません。すべての計算はフロントエンドで行われます。つまり全てがブラウザ上で実行されているということです。

全てのソースコード は GitHub に置いています。

 

今後の展望

最初にも書いたように、私が skiMogul を書き始めた動機は ラムダ計算の入門者のハードルを下げること です。だいたい6~10歳くらいの小学生がシンプルに自然にラムダ計算を学び始められる世界を夢見ています。
skiMogul の機能について要望・意見があれば教えてもらえると嬉しいです。

 

 

私からは以上です。


  1. Unlambda には3つの主要な関数以外に、評価順を制御する d 関数や印字用の関数群なども多数組み込まれていますが
  2. ただし、チャーチ数と云われる関数として表現された数です