2021年4月第2週レポート

先週は急性胃腸炎にかかってしまい、Weekly Reportを一週スキップしてしまいました。体調はバッチリ回復したので、今週からまたレポートを再開します。

インプット

📝 「Hands-On Functional Programing with TypeScript」を読んだ

これまで読んでいたJavaScript関数型プログラミングだけではスッキリ理解できなさそうだったので、別の本を読んでみました。こちらはTypeScriptで関数型プログラミングをやってみよう、といった趣旨の本で、かなり初級者向けに書かれています。

前半はTypeScript、というよりはJavaScriptの基礎が開設されており、関数のスコープやクロージャ、非同期処理といった一般的な機能についての解説にページが割かれています。後半からは関数型のトピックとなり、関数合成、部分適用、カリー化といった関数そのものを操作する方法からはじまり、その後圏論と関わりがある、ファンクター、アプリカティブ、モナドの概要が紹介されています。また、モナドの例としてMaybeEitherについて解説されています(なお、本書では基本的にライブラリを使わず、全て一から実装するスタイルをとっています。)。その後はレンズやプリズムといった不変性に関連するテクニックの紹介、関数型リアクティブプログラミングの例としてRxJSの基本的な使い方が紹介されていました。

かなり丁寧に解説されているので理解は容易ですが、難易度自体が低いのでこれでバリバリ関数型プログラミングできるぜ、とはなりませんが、とっかかりとしてはいいのかなあ。

📝 Conditional Typesについて調べた

TypeScriptで関数型プログラミングをやってみるためのライブラリとしてfp-tsを使っているのですが、ただ単に使うだけでなくその実装も確認しながらの方が得るものが大きいです。実装自体もほとんどが数行の関数なので読むだけならそれほど苦ではありません。

ただ、TypeScriptの型定義周りの機能を駆使して実装されているので、前提としてその知識を持っていないと理解は大変かもしれません。fp-tsだとConditional Types(条件型?)が結構使われているので、再確認のためにも調べました。

Conditional Types

  • 型レベルの条件分岐が可能な型
  • T extends U ? X : Yと表現され、TUの部分型ならばXに、そうでなければYになる型
type Diff<T, U> = T extends U ? never : T
const a : Diff<'hoge' | 'foo' | 'piyp', 'foo'> = 'hoge'

3つのstringリテラルUnionからfooを除外した、要は差集合のような型をあらわすサンプル。

Conditional Typeの特徴として - XYの決定に対してTUという型変数に依存がある場合、型の解決はTUが決定されるまで評価が遅延される - 先の例だとUにマッチしない場合はT型となるので具体的な型が与えられない限り型は確定しない - Union typesの分配則 - Union typesのConditional Typesは、Conditional TypesのUnionに展開される - すなわち、(T1 | T2) extends U ? X : Y = (T1 extends U ? X : Y) | (T2 extends U ? X : Y)のように展開される

ある型について、再帰的にReadonly化する型

参考にしたドキュメントにて、ある例が提示されていたのでそれを自分なりに解釈してみた

組み込みのReadonly<T>は浅くreadonly化する型で、オブジェクト型のプロパティのプロパティや配列型プロパティの要素はreadonlyとならない。これを解決するため深くreadonly化を実現する型をConditional Typesで作る

まずはある型からFunction型以外のプロパティの名前のUnion Typeを表現する型を用意する

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]
  • keyof TTのすべてのプロパティの名前のstring literal union types
  • {[K in keyof T]: T[K]}はMapped Typesであり、TのプロパティのKの型はT[K]である、すなわちT型そのものを表している

{[K in keyof T]: T[K] extends Function ? never : K}もまたMapped Typesと言える。この型はT型がもつプロパティと同じプロパティを持っているが、そのプロパティの型がConditional Typesで表現されている。その内容はTのプロパティKの型T[K]Function型であるときはneverとなり、そうでないときはK(string literal)となる、というもの。

例えば以下のようにしてA型を定義すると

type NonFunctionProperties<T> = { [K in keyof T]: T[K] extends Function ? never : K }

interface Student {
    name: string,
    age: number,
    greet: () => string
}

type A = NonFunctionProperties<Student>

A型は次の型と同一となる

interface A {
    name: 'name'
    age: 'age'
    greet: never
}

NonFunctionPropertyNamesの話に戻って、末尾の[keyof T]によって{[K in keyof T]: T[K] extends Function ? never : K}がUnion Typesに展開されるので

type A = NonFunctionPropertyNames<Student> // 'name'|'age'

となる。

これを使って再帰的にreadonly化するDeepReadonly<T>を定義すると

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]

type DeepReadonly<T> =
  T extends any[] ? DeepReadonlyArray<T[number]> :
    T extends object ? DeepReadonlyObject<T> : T

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {
}

type DeepReadonlyObject<T> = {
    readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>
}

infer

  • Conditional Typeでは、条件定義で導入した型変数を、結果の型に使用することができる
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;

ReturnType<T>Tが関数であるとき、その関数の戻り値の型を表すようなジェネリクス型となっている。このときinferキーワードで修飾した型変数を分岐後のthenサイドで利用することができる(else側では使えない)

またinferキーワードは複数箇所、複数種使うことができる

type Foo<T> = 
    T extends { 
        foo: infer U;
        bar: infer U;
        hoge: (arg: infer V)=> void;
        piyo: (arg: infer V)=> void;
    } ? [U, V] : never;

interface Obj { 
    foo: string;
    bar: number;
    hoge: (arg: string)=> void;
    piyo: (arg: number)=> void;
}

declare let t: Foo<Obj>; // tの型は[string | number, string & number]

参考

TypeScript: Documentation - Conditional Types (typescriptlang.org) TypeScriptの型入門 - Qiita TypeScriptの型初級 - Qiita

📝「平凡なプログラマにとっての関数型プログラミング」を読んだ

平凡なプログラマにとっての関数型プログラミング | anopara

最近、関数型プログラミングにかぶれているわけですが、そのモチベーションがこちらの記事に書かれていることに近いものであり印象に残っています。

自分が関数型プログラミングを勉強しているのも、別にオブジェクト指向プログラミングに絶望したからといわけではなく、単にコードを設計/記述するときに切れるカードを増やしたいからです。実際、自分が開発しているプロダクトのコードにもEitherのようなものでエラー処理を書いたり、ReactiveExtensionsを使ったリアクティブプログラミングに影響を受けていたりなど、関数型プログラミングの考え方に近いものを使うようになってきています。

そういったものを使っていくなかで、もっとスマートにコードを書く方法があるんじゃないのかという思いがだんだんと自分の中で沸き上がってきました。そこで頭に浮かんだのが、自分のコードに少しずつ影響を与えている関数型というプログラミングパラダイムが改善のよいきっかけになるのではないか、というものです。とはいえ、オブジェクト思考プログラミングを切り捨てたいというわけではないです。前節で紹介した書籍でもオブジェクト指向と関数型指向は相反するものではなく、両者の良いところをうまくミックスするのが重要であると述べられていました。

本当に良い影響があるかはまだわかりませんが、まずは知ることからはじめないとどうしようもないので、これからも勉強は続けていきたいと思います。

📝 「Denoのフロントエンド開発の動向【2021年春】」を読んだ

Denoのフロントエンド開発の動向【2021年春】 (zenn.dev)

Denoが登場してしばらくがたちますが、Denoを使ったフロントエンド開発も少しずつ発展しているようです。自分自身、Denoをそこまで使っていない状況なのですが、うかうかしているとトレンドに追いていかれそうなので要注意です。

アウトプット

目立ったものはなし

Profile
d_yama
元Microsoft MVP for Windows Development(2018-2020)
Sub-category : Windows Mixed Reality
Search