Add blog (1)

This commit is contained in:
2025-02-10 18:53:16 +09:00
parent d30cc44ab7
commit 3cfdcd1fe2
25 changed files with 2653 additions and 24 deletions

View File

@@ -18,9 +18,16 @@
"@sveltejs/enhanced-img": "0.4.4",
"@sveltejs/kit": "2.15.1",
"@sveltejs/vite-plugin-svelte": "4.0.4",
"@types/prismjs": "1.26.5",
"autoprefixer": "10.4.20",
"katex": "0.16.21",
"mdsvex": "0.12.3",
"misskey-js": "2024.11.1-alpha.0",
"prismjs": "1.29.0",
"rehype-katex": "7.0.1",
"rehype-katex-svelte": "1.2.0",
"remark-footnotes": "2.0",
"remark-math": "3",
"schema-dts": "1.1.2",
"svelte": "5.16.1",
"svelte-check": "4.1.4",

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import { page } from "$app/state";
import { limitWidth } from "$lib/constants";
import { cn } from "$lib/utils";
import LuCopyleft from "lucide-svelte/icons/copyleft";
let { children } = $props();
</script>
<div class="flex flex-col min-h-screen items-center gap-12 lg:gap-16 py-8">
<header class="flex flex-col items-center w-full gap-4">
<div class="font-display text-4xl md:text-5xl lg:text-6xl">
{#if page.url.pathname === "/"}
cannorin.net
{:else}
<a href="/">cannorin.net</a>
{/if}
</div>
</header>
{@render children()}
<footer class={cn(limitWidth, "mt-auto flex items-center flex-col gap-1 lg:flex-row lg:gap-2 lg:justify-center text-xs")}>
<p class="flex gap-1 items-end">
<LuCopyleft aria-label="Copyleft" size=12 /> 2024 cannorin. Some rights reserved.
</p>
<p>
The text of this website is licensed under <a class="underline" href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC BY-SA 4.0</a>.
</p>
<p>
Code examples are licensed under the <a class="underline" href="https://spdx.org/licenses/MIT.html" target="_blank">MIT License</a>.
</p>
</footer>
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { limitWidth } from "$lib/constants";
import { cn } from "$lib/utils";
import type { Snapshot } from "./$types";
import type { Snapshot } from "../$types";
import CardDev from "./dev/card.svelte";
import CardMath from "./math/card.svelte";
import CardMusic from "./music/card.svelte";
@@ -30,9 +30,7 @@ export const snapshot: Snapshot<SnapshotData> = {
</script>
<!-- Sections -->
<main class={cn(limitWidth, "flex grow flex-col items-center gap-12 lg:gap-16 py-8")}>
<h1 class="font-display text-4xl md:text-5xl lg:text-6xl">cannorin.net</h1>
<main class={cn(limitWidth, "flex flex-col items-center gap-12 lg:gap-16")}>
<section class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-8">
<h2 class="sr-only">自己紹介</h2>

View File

@@ -0,0 +1,7 @@
import { data } from "./data";
export async function load({ url }) {
const slug = url.pathname.split("/").slice(-1)[0];
const path = `/src/routes/(main)/blog/(articles)/${slug}/+page.md`;
return { ...data[path].metadata, slug };
}

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import "prismjs/themes/prism.min.css";
import "katex/dist/katex.min.css";
import { limitWidth } from "$lib/constants";
import { cn } from "$lib/utils";
let { children, data } = $props();
let date = new Date(data.date);
</script>
<main class={cn(limitWidth, "flex flex-col items-center gap-12 lg:gap-16")}>
<article class="w-full max-w-[720px] space-y-12">
<header class="flex flex-col gap-4">
<h1 class="text-2xl">
<span style="view-transition-name: article-{data.slug}-title">
{data.title}
</span>
</h1>
<div class="text-sm flex flex-wrap-reverse justify-between" style="view-transition-name: article-{data.slug}-meta">
<ul class="flex gap-3">
<li class="text-primary font-bold">#{data.category}</li>
{#each data.tags ?? [] as tag}
<li>#{tag}</li>
{/each}
</ul>
<time datetime={date.toISOString()}>{date.getFullYear()}.{date.getMonth() + 1}.{date.getDate()}</time>
</div>
</header>
<div class="prose prose-sm prose-light !max-w-fit">
{@render children()}
</div>
</article>
</main>

View File

@@ -0,0 +1,78 @@
---
title: Dependent Types と Refinement Types の違い
description: Dependent Types (依存型) と Refinement Types (篩型) は目指すところは似ているが,導入スタイルに根本的な違いがある
date: !!timestamp 2018-07-07T05:42:46+09:00
category: Programming
tags: ["Type System"]
---
自分の tweets.zip をみてたら発掘された文章.
Dependent Types (依存型) と Refinement Types (篩型) は目指すところは似ているが,導入スタイルに根本的な違いがある.
## TL;DR
Refinement types は (Curry-style typing のように) extrinsic でDependent types は (Church-style typing のように) intrinsic である.
## 前提
一般に,プログラム言語への型の導入スタイルには Curry-style と Church-style の2種類がある
* Curry-style: 型付けは *既に存在する項を* 分類するもの
* Church-style: *項が存在するとは* 型付けされているということ
前者は動的型付き言語での型アノテーションなどが当てはまり,型アノテーションを全部取り除いたとしてもプログラムの意味は全く変わらない.それは型というものが *外因的に(extrinsically)* 追加されたものだから.
後者は一般的な静的型付き言語が当てはまり,その中では型検査が通らないプログラムは invalid である.それは型というものがそもそも言語の一部 (計算対象となる "項") であり,*内因的に(intrinsically)* 存在しているものだから.
## Dependent Types
Dependent types とは,項に *依存して* 決まる型である.
たとえばIdris における固定長リスト(ベクトル)の定義は次のような形をしている.
```haskell
data Vect : Nat -> Type -> Type where
Nil : Vect Z a
(::) : a -> Vect n a -> Vect (S n) a
```
ここで `Vect` は型変数 `a` だけでなく,リストの長さを表す自然数 `n` をパラメータとして取る.すなわち自然数の項が与えられなければ型を構成できない.この点で型 `Vect` は項に依存しているといえる.
なお一般に普通の関数は項に依存する項多相型を持つ項は型に依存する項型コンストラクタは型に依存する型でありdependent types はこの関係性のぽっかり空いた穴を埋める存在でもある.
## Refinement Types
Refinement types とは型を述語で *修飾する* ものである.
たとえばLiquid Haskell において `Data.Vector` を長さで修飾するには次のようにする.
```haskell
module spec Data.Vector where
import GHC.Base
measure vlen :: (Vector a) -> Int
assume length :: x:(Vector a) -> {v: Int | v = (vlen x)}
assume ! :: x:(Vector a) -> {v: Int | 0 <= v && v < (vlen x)} -> a
```
ここでは `Vector a` の持つ property として `vlen` を定義しており(制約中に書けるように)`length` 関数で得られる値が `vlen` と等しいこと,`!` 関数でインデックスアクセスをするときはインデックスが 0 以上 `vlen` 未満であることをそれぞれ仮定している.
Dependent types と大きく違うのは,こちらでは(実行時に)行われるのは本来の `Data.Vector` の動作そのものであり,ここで定義したこととは何も関係がないし,取り除いても全く同じ動作をする.また本来の `Data.Vector` の実装を検証しているわけでももちろんない.
## つまり
Dependent types は言語の一部である一方refinement types は言語の外側にある.この関係は Church-style typing と Curry-style typing との関係と同じかたちをしている.
そもそも Curry-style は型なしの項を *修飾* しているものであるからrefinement terms と呼ぶこともできる.一方 Church-style を雑に (type-)dependent terms と呼んでもそんなに間違ってはいないだろう(そもそも term/value-dependent types である)
これを使えば,プログラム言語のスタイルを dependent/refinementtypes/terms で 2x2 通り (+依存型も篩型もないもの) に大まかに分類することができる.
| | D. Types | R. Types | (None) |
| ------------ | -------- | -------- | ------ |
| **D. Terms** | Dependent ML | Liquid Haskell | (ML) |
| **R. Terms** | NuPRL | ??? | (LISP) |
なおdependent types と refinement types を両方持っていてもよくF* などがそのような言語で実用的なものの例である.

View File

@@ -0,0 +1,299 @@
---
title: エラーメッセージの読み方と対処,検索や質問の原則
description: どういうエラーメッセージが出たときは何が原因で,どのように対処すれば解決するのか.その知識・経験の積み重ねこそがあなたを一人前のプログラマにするのだ
date: !!timestamp 2019-01-23T00:00:00+09:00
category: Programming
tags: ["Misc"]
---
> この記事は [以前 Qiita に投稿した記事](https://qiita.com/cannorin/items/eb062aae88bfe2ad6fe5) の転載です.
プログラミングをしている限り,エラーメッセージに遭遇するのは避けられないことだ.そこで,あなたは周りのできる人に「エラーが出ました」と言って "答え" を聞こうとするだろう.
でも,もし聞ける人が誰もいなかったら? もし,周りの誰にもわからないようなエラーにぶつかってしまったら?
あなたが一人前のプログラマになるためには,**自分で**エラーメッセージを読んで,解決できるようにならなければならない.
どういうエラーメッセージが出たときは何が原因で,どのように対処すれば解決するのか.**その知識・経験の積み重ねこそがあなたを一人前のプログラマにする**のだ.これは安直に "答え" だけを追い求めていてはいつまで経っても身に付かない.
## エラーメッセージの大原則
まず最初に頭に入れておくべきなのは,**エラーメッセージは意味不明な暗号ではない** ということだ.その実,エラーメッセージはかなり簡単な英語で書かれている.決まった用語や,決まった形の文を使いまわしている.それらを覚えれば,未知なるエラーメッセージに遭遇したとしても,なんとか解釈することができる.**エラーメッセージは読めるし,読むべきものだ.**
### 1動詞を含む短い文を探す
大量のエラーメッセージを前にしたプログラミング初心者は,とかくその量に圧倒されてしまいがちだ.まずは一旦落ち着いて,**動詞を含む短い文** を探すとよい.
* 'i' undeclared (first use in this function)
* parse error before '}'
* undefined reference to 'prnitf'
* unterminated string or character constant
このような短い文章がエラーメッセージの本質的な部分だ(大体は undefined などの動詞から始まる).近くにエラーが発生したファイル名や行番号が表示されていることが多い.
見て分かるとおりこれらは非常に単純な英語でほとんどの場合10語未満だ使われる単語もほぼ決まっているので読めるようになるべきだ**もし読めない場合でもGoogle 翻訳を使ってでも自力で読むようにしよう.** 実際に Google 翻訳にかけると,
* 'i'が宣言されていません(この関数で最初に使用されます)
* '}'の前のパースエラー
* 'prnitf'への未定義の参照
* 終端されていない文字列または文字定数
となる.この時点で対処方法がはっきりわかるかもしれないし,そうでなくても,少なくともどういうことを言っているのかは理解できるはずだ.
### 2ファイル名が書いてあるときはそのファイルに原因がある
当たり前のようだが,エラーメッセージにファイル名が出てきているとき,**まさにそのファイルに問題がある**.行番号が表示されているならば,ほとんどの場合,まさにその行にエラーがある.
次にするべきなのは,その部分や周囲(大体は前)に間違っている部分がないかチェックすることだ.上で見つけた文と合わせれば,どう間違っててどうすれば解決するのかすぐにわかる場合が多い.
"at" や "before" といった位置を表す前置詞にも注目.関数の定義とその関数を呼び出す部分で何か食い違いがある時,"from" などの前置詞を使って呼び出し元の位置を表示していることもある.
### 3長いエラーメッセージは上から順番に読む
もしエラーメッセージがとてつもなく大量に出てきたとしても慌てる必要はない.
大多数のコンパイラはソースコードを上から読む.すると**ソースコードの上の方で見つかったエラーほど先に報告する**から,エラーメッセージの上の方に表示されることになる.
上の方にあるコードというのはそれより下のたくさんの部分で使われている(可能性がある)から,エラーが影響する範囲が大きくなる.だから,**エラーは上から順番に対処したほうがよい**のだ.
しかも,上の方にあるエラーが原因で,それを使う他の部分でもエラーが起こっていることもある.そのようなエラーは根本的なエラーを直したら消えてしまうことが多い.その意味でも,上の方にあるエラーを先に直すべきだ.
## エラーの解決の大原則
エラーメッセージを読めたら,次は解決を試みる.エラーの原因を探るのにはいくつかのコツがある.自分でエラーを解決する経験を積み重ねて自分なりのコツを編み出すことで,プログラミングは上達していく.
### 1何をすればエラーメッセージが出てくる・出なくなるのかを見極める
エラーがある箇所はなんとなく分かるけど,直し方の検討が付かない場合は,**該当の部分を取り除いてエラーが出なくなるかを確かめる.** それでエラーが出なくなれば詳しく調べる必要があるし,何か別のエラーが出始めた場合はそれを先に直す.もしかしたらそっちが原因かもしれない.
### 2エラーが再現する小さなプログラムを作る
エラーが起きる箇所だけを取り出して,**そのエラーが出るような,できるだけ小さいプログラムを新しく作ってみる.** そしてそのプログラムがどのように動いているのかを確認して,何故エラーが出るのかを考えていく.
これは大きなプログラムを作っている場合は特に有効だ.一見遠回りのようだが,**エラーの原因となる部分に集中することができるから,結果的にはエラーの解決が早くなる.**
このような,同じエラーが出るできるだけ小さなプログラムのことを,[**最小再現コード (MRE)**](https://en.wikipedia.org/wiki/Minimal_reproducible_example) という.エラーを出すために必要な要素だけが詰まっているために詳しく調べやすいので,後述の Google 検索や質問サイトを使う時は,この MRE を使うとよい.
### 3プログラムの動作の流れを追いかける
コンパイル時にエラーメッセージが出てくれるならばよいのだが,コンパイルは通ったのに実行してみるとエラーが出る,ということも多い.そもそもコンパイラがない言語ならばなおさらだ.
そういう場合は,プログラムが実際にどのように動いているのか,どのコードがどういう順番で実行されているのかを把握することで,エラーの原因を特定しやすくなる.
プログラムを動かしてみてエラーの原因(bugバグ)を探る作業を **デバッグ(debug)** という.デバッグにはいくつかの手法がある.
* `printf` デバッグ
* エラーの原因となっていそうな変数があるとき,`printf` などの画面に表示させる関数を使って,その変数の値を処理の途中途中で表示させる.
* どこかのタイミングで変数の値が意図しないものに変わったならば,そこが原因だ.
* どこに書き加えた `printf` なのかが分かるように,出力を工夫するとよい.
* デバッガを使う
* 言語によっては,デバッガと呼ばれる,プログラムの実行を途中で止めたり,変数の値を覗いたりすることができるプログラムを使える.
* C言語ならば GDBPython ならば PDB自分の使っている言語向けのデバッガが何かは自分で調べる
* Visual Studio などの統合開発環境(IDE)と呼ばれるエディタを使っているならば,標準でデバッガが搭載されていることが多い.
* `printf` デバッグより取っ付きにくいかもしれないが,こちらのほうが便利だ.使って慣れておこう.
大きなプログラムをデバッグするのは大変だが小さなプログラムならば多少デバッグしやすくなるMRE を作ると,デバッグの役に立つ.
### 4仮説・予想・実験・考察のサイクルを回す
愚直にデバッグしていてもエラーの原因は見つかるかもしれないが,何故エラーが出るのかを考えながらデバッグしたほうがよほど速く見つかる.そこで,
1**エラーの原因の仮説を立てる**
2もしその原因が正しかったと仮定して**きちんと動く場合/エラーになる場合 を予想する.**
3その場合になるように実際に動かしてみて**予想通り成功/失敗するか確かめる.**
4予想と違った場合**改めて考え直して,違う仮説を立てる.**
というサイクルを回すことで,闇雲に探すよりも速く原因を特定することができる.例えば入力した数式を計算するプログラムを作っていて,計算結果が合わない場合は,
1掛け算より足し算が先に計算されているかもしれない
2もしそうなら`1+2*3``9` になるはずだ.
3実際に `1+2*3` を入力して計算してみる.
4`7` だった.原因は他にある.
という具合だ.
なお,やらかしそうな間違いをあらかじめ予想しておいて,**実際にやらかした場合に気付けるように,簡単に実験を回せるようにしておくのが,テストを書くということ**である.
### 5これらを組み合わせる
原則2~4はお互いを高め合う効果がある
* MRE を作れれば,コードが短くなってデバッグがしやすくなるし,考慮するべき物事が少なくなるので,仮説を立てやすくなる.
* デバッグでプログラムの動作を把握すれば,仮説が立てやすくなるし,エラーと関係ない部分が分かって MRE を作りやすくなる.
* 的確な仮説を立てられるようになればMRE 作りやデバッグが手探り状態ではなくなる.
* 仮説を複数立てて,それぞれに対応した MRE を書いてみて,どれが本当にエラーになるのか(もしくはどれもならないのか)を調べることができる.
* そして,"当たり" の MRE をデバッグして直すことができる.
### 6エラーは1つずつ解決する
**一気に全部のエラーを解決しようとは絶対にしないこと.** 複数のエラーを解決しようとすると MRE が作れなくなるし,考えることがあまりにも多くなって頭がパンクし,何も分からなくなる.
**困難は分割せよ.** 慌てる必要はない.大量のエラーメッセージも上から順番に一個一個解決していくのが一番早い.一見ひとつの大きな問題に見えることでも,実際は複数の小さな問題が同時に起こった結果だ.それを見極め,細かく切り分けて,一つ一つ対処する.
### 7はじめからやり直せるようにしておく
ファイル入出力を行うプログラムや Makefile など,ファイルの読み書きが発生するものをデバッグする時には,実行前と後で状態が変わってしまうので,**実行する前の状態でディレクトリごとコピーしておいて,めちゃくちゃになっても最初からやり直せるようにしておく.**
デバッグしているうちに何がどうなっているのか,自分が今何をやっているのか,さっぱりわからなくなるのはよくあることだ.そこで一旦考えをリセットするために,**動いていた・分かっていた段階でバックアップを取っておこう.** もちろん,"エラーは1つずつ解決する" 原則を守っていれば,そういう事態は起こりにくくなる.
なお,この原則はバージョン管理システム(git など)の考え方に直接繋がる.早めに使えるようにしたほうがよいだろう.
## Google 検索の大原則
エラーを自力で解決できない時にはGoogle などの検索エンジンを使って調べることになる.
どんなに経験を積んだプログラマであっても,エラーメッセージが出たときに Google 検索に頼ることはある.単純に今まで見たことがなかったり,起こらないと思っていた箇所で発生していたり,そういう場合だ.
しかし経験を積んだプログラマは当たり前だけどGoogle 検索のやりかたにも経験を積んでいるGoogle 検索は便利なツールだけれども,適切な検索キーワードを入れてやらないと,必要な情報にたどり着けないことが多い.彼らはそのうまいやり方を知っているのだ.
**どういうキーワードで検索すれば欲しい情報が見つかるのか,その感覚を身につける**ことがプログラミング上達への近道だ.
### 1"動詞を含む短い文" をそのまま Google 検索に入れる
前述の "エラーメッセージの大原則" で探すように述べた "動詞を含む短い文" をそのまま Google 検索に入れてみるのはかなり賢い選択だ.何故なら,それがまさにエラーの本質的な部分だからだ.
こういう文は短くて特徴的で目立つから,エラーについて解説しているサイトや,そのエラーについて質問しているページなどでも,その短い文をエラーの名前として使っているものがほとんどだ.
### 2エラーコードで検索する
エラーを分類するコードやIDのようなものが表示されていたらそれを検索キーワードに使うのも有効だ例えば
`error CS0246: The type or namespace name 'MyClass' could not be found.`
というエラーだったら,`error CS0246` で検索してみるのもよい.エラーコードは英語のプログラミング環境でも日本語のプログラミング環境でも同じものが使われるので,日本語の解説がある場合にそれが出てきやすくなる.
### 3ファイル名は検索キーワードに入れない
ファイル名・ディレクトリ名はあなたが勝手に決めたものである場合がほとんどで,インターネット上の誰かが全く同じファイル名を使っている確率はゼロだ.というわけで,それらは検索キーワードに入れない.
一般に,**あなたが勝手に決めた名前はなるべく検索キーワードに入れないようにするべき**だ.先ほどの例で言えば,`MyClass` なんて名前を付けている人が他にいて,あなたと同じエラーに遭遇している確率はゼロに近いだろう.だから,`The type or namespace name 'MyClass' could not be found.` で検索するより,そういう部分を除いた `The type or namespace name could not be found.` で検索したり,エラーコード `CS0426` で検索したほうが良い結果が得られる.
### 4情報を絞り込むキーワードを追加する
原因が違っていても同じエラーメッセージが出ることはそれなりにある.そのせいで,単純にエラーメッセージで調べても,違う状況について解説したページばかり出てきて,本当に欲しい情報が出てこないことがある.そういうときは,情報を絞り込むためにいくつかキーワードを追加する.
キーワードには,**今直面している状況の(エラーが出なかった時と比べた)特殊さを端的に表現するもの**を使う例えばソースコードファイルが1つしかなかった時は出なかったエラーがファイルを増やした途端出てきたような場合は「複数ファイル」などのキーワードを追加するファイルを読み書きするようなプログラムを書いていて名前が英数字なファイルは開けるのに日本語が入っていたら開けないという状況になったならば「ファイル名 日本語」などのキーワードを追加する.
また,そのような**キーワードを選ぶ際には,本当にそれが原因なのかどうかをあらかじめ確かめておく**必要がある.もしそれが原因ではなかったならば,全く見当違いな情報が出てくることになるからだ.ソースコードファイルを増やしたらエラーが出てきたと思うなら,一旦ファイルを減らしてみてエラーが消えることを確認するべきだ.上述の **[MRE](#2エラーが再現する小さなプログラムを作る) を作っておけば,この作業は要らなくなる.**
### 5英語で調べてみる
日本人は1億人ちょっとしかいないが英語の話者は18億人もいる当然日本語で書かれた情報よりも英語で書かれた情報のほうが何倍も多い日本語で対処方法を解説したページが全く見つからないときでも**英語でならば,ほとんどの場合,何かしらの情報が見つかる**
プログラマをやっていると,どうしても英語の情報を読まなければならない状況は必ずやってくる.そういうときに,英語だから,と尻込みしていても何も解決しない.
エラーメッセージと同様に,**Google 翻訳を使ってでも自力で読む習慣を付けよう**.読める人に聞いているばかりでは,いつまで経っても自力で解決できるようにはならない.
### 6サンプルコードを見つけても何も考えずにコピペしないこと!!
それが**何をしていて,何故動くのかを自分で考えて**,それに基づいて自分のコードを書き直すこと.
何度も言っているように,他人から得た "答え" を書き写しているだけでは,いつまで経っても身に付かないし,それを自力で書けるようにはなれない.
## 誰かに質問するときの大原則
Google 検索で探しても欲しい情報が得られないことも時々ある.自力で解決できる見込みがどうやらなさそうな場合は,他人に質問するしかない.
しかし,本当に欲しい情報を得るためには,質問をする場所や,質問の仕方にもコツが必要だ.暇なプログラマはそれなりにいるが,どこの馬の骨とも知れないサイトに投稿された,わけのわからない質問に答えてくれるほど暇ではない.
### 1プログラミング専用の質問サイトを使う
経験を積んだプログラマが生息するウェブサイトには偏りがある.彼らは大抵の場合,プログラミング専用の質問サイトしか見ていない.
プログラミング専用の質問サイトで,日本語話者向けのものをいくつか紹介する.
* [teratail【テラテイル】ITエンジニア特化型Q&Aサイト](https://teratail.com/)
* 初心者でも気軽に質問できるサイト.
* 質問のテンプレートや初心者マーク機能などがあるので,初めて質問サイトを使う場合でも安心.
* ここで質問の仕方に慣れるといいだろう.上達してきて回答者側になる時も,最初はここがオススメ.
* [スタック・オーバーフロー (Stack Overflow)](https://ja.stackoverflow.com/)
* 英語圏で有名な老舗プログラミング質問サイトの日本語版.
* 上級者が集まる傾向がある.ある程度プログラミングが上達してきて,それでもわからないことがある時に使うとよい.
* かなり上達してきたプログラマは,本家の英語版しか使わなくなる.一般的にレベルが高く信頼できるサイトとされているので,読むだけでも慣れておこう.
* [Qiita(キータ)](https://qiita.com/)
* 厳密には質問サイトではなく,プログラミングの知識や,問題に遭遇したときの解決方法を,記事にしたためて共有するためのサイト.
* 質問自体を投稿するというよりはエラーの原因が分かって解決した後にエラーを出してしまった原因と対処方法をまとめて1つのエラー対策記事として投稿するとよい
* 他の人から「このやり方のほうがよい」などのアドバイスがもらえることもある.
* 技術系の記事を書く練習にもなるQiita の記事は玉石混交なので,あなたの書いた記事のクオリティが低くてもあまり恥ずかしがることはない.しかし,他人が読んで分かりやすい文章を書けるように努力・練習するべき.
一方,"Yahoo!知恵袋" や "教えて!goo" などの**一般質問サイトを使うのは本当にやめたほうがいい**["教えて君"](http://www.redout.net/data/osietekun.html) と ["教えてあげる君"](https://www.weblio.jp/content/%E6%95%99%E3%81%88%E3%81%A6%E3%81%82%E3%81%92%E3%82%8B%E5%90%9B) が跳梁跋扈していて,まずまともな回答が来ない.
プログラミング専用の質問サイトを使う場合でも,["教えて君"](http://www.redout.net/data/osietekun.html) になってしまわないよう気を付けよう.**プログラマは "教えて君" が本当に嫌い**なので,専用サイトにしか生息していないのだ.
### 2質問する前にまず検索する
プログラミング専用質問サイトには,毎日のようにプログラミングに関する質問が投稿されては解決されている.**あなたが初心者であればあるほど,他の初心者と同じ問題にぶつかっている可能性が高い.** つまり,同じような質問が既にされている可能性も高い.
Google 検索で出てこなくても,質問サイト内検索では出てくるかもしれない.**質問を投稿する前に,もう一度検索してみよう.** "Google 検索の大原則" はここでも活きる.
### 3エラーメッセージは原文通りに貼り付けて実行環境を詳しく書く
経験を積んだプログラマは,エラーメッセージの中からあなたが見逃した部分にしっかり気づいて,対処方法を教えてくれる.だから,**エラーメッセージの一部ではなく,全部を貼り付けるようにする.**
もしエラーメッセージに個人的なファイル名が含まれていてそのまま投稿するのが恥ずかしい場合は,そこだけ隠してしまっても構わない.例えば `C:\Users\Tarou\練習\main.c` なら,`C:\Users\XXX\XXX\main.c` のように隠す.ただし,肝心のソースコードのファイル名はなるべく隠さないこと.また,ファイル名がエラーの原因になっていそうな場合は,当然ながら隠さないほうがいい.
また,**どのような環境でそのエラーが出たのかを詳しく書くことが大事**だ.特定の環境でしか出ないエラーはよくあるが,他の人があなたと同じパソコンを使っている可能性はゼロだ.どういう環境でプログラミングしているのかは,書いてくれないとわからない.
* **使っている OS** (WindowsMacetc)
* できれば OS のバージョンも書いたほうが良いOS バージョンの調べ方が分からないなら,自分で検索する.
* **コンパイラやインタプリタのバージョン**
* 例えばC言語を使っていて GCC でコンパイルしているならば,`gcc -v` というコマンドを実行すればバージョンを表示させることができる.
* Python なら `python --version`.使っているプログラミング言語のバージョンの出し方が分からないならば,まずそれを検索すること.
* エラーメッセージと同じで,出力をそのまま全部載せる.
* **プログラムをコンパイルしたり実行したりする時に打ったコマンド**
* 他の人が質問を見て,同じようなプログラムを同じようなやり方で実行できる程度には,どういう風に実行したかを書いておくとよい.
エラーを解決するのに大事な要素に "再現性" がある.エラーに再現性があるとは,あなた以外の人も同じエラーを出せる,という意味だ.**どのようにすればそのエラーを再現できるのかを他の人に的確に伝える**ことができれば,解決方法を探すのを手伝ってくれる.
上述の **[MRE](#2エラーが再現する小さなプログラムを作る) を作っておくと,他の人も試しやすくなる.** MRE を他人に渡す際は,数ファイル程度の小さいものは [Gist](https://gist.github.com/) などに,何十ファイルもある大きいものは [GitHub](https://github.com/) などに置くとよい.特に Web 系は最小構成であっても各種 config でファイル数が多くなりがちなのでGitHub に置いたほうがよいだろう.
### 4自己解決した場合はその解決方法を書き残す
ある程度プログラミングに上達してくると,質問はしたけれど誰かが答える前に自分で解決できた,ということが起こる.その時に,**「解決しました」とだけ書き残して去ることだけは絶対にやってはいけない.**
どのようにすれば解決するのかをきちんと書き残すことで,また同じエラーが出た時にそこを見ればよくなるし,**同じようなエラーが出た他の人の助けになる**
### 5複数の質問サイトに同じ質問を投稿しない
上と似たような理由で,複数の質問サイトに同じ質問を投稿するのもやめたほうがいい.
回答が付かなかったサイトにも解決方法を書き残す必要が出てきて面倒だし,かといってそれをしなければ,回答のない質問が検索に出てくることになって,**解決方法が載っているものにたどり着きにくくなる**だから最初から1つの質問サイトで質問したほうがいい
もしいくら待っても全然回答が付かなくて,他のサイトで聞いたほうが早そうな場合は,**あくまでも最後の手段として**,「あのサイトに投稿したこういう質問に答えてくれませんか?」という質問を(もちろん対象の質問の URL 付きで)投稿するとよい.
### 6答えてくれた人に感謝の気持ちを伝える
当たり前だけど,答えてもらってお礼を言わないのは非常に印象が悪い.一言でいいので感謝のコメントを付けておこう.
### 7サンプルコードをもらっても何も考えずにコピペしないこと!!
もう説明は要らないはず.
## その他
* **日本語と英語の能力は,時としてプログラミング言語の能力よりも重要になる.**
* **プログラムは思ったとおりには動いてくれないかもしれないが,少なくとも書いたとおりには動いている.** 自分が今何を書いているのかを,できるだけ把握する.
* **人に教えることが一番勉強になる.** 自分が理解していることを,他人にわかるようにきちんと文章に起こすことで,あまりよく分かっていない部分に気づいたり,感覚でやっていたことが明文化されたりして,自分の理解が一層深まるからだ.
* プログラミングが上達してきたら,今度は質問サイトで答える側に回ってみよう.
* 教える側と教えられる側に,上下関係・偉い偉くないは(本質的には)ない.教える側のほうがより難しく厳しい勉強をしているだけのこと.お互いにきちんとリスペクトしあい,自分の立場に責任を持つこと.
* **"自力で考える・自力でやってみる" 姿勢が一番大事.**
* 自力で何もしなくなったプログラマの成長は,残酷なほどすぐに止まる.
## 参考リンク
* [ハッカーになろう (How To Become A Hacker) - Eric SRaymond](https://cruel.org/freeware/hacker.html)
* 著名なハッカー(優秀なプログラマの意)の Eric S. Raymond が,ハッカーとしての心構えや行動指針を語る.
* 優秀なプログラマは多かれ少なかれ,似たような思想を持って,似たようなことをしている.参考・目標にしよう.
* [プログラマの心の健康 - 結城浩](http://www.hyuki.com/kokoro/)
* 「数学ガール」シリーズで有名なプログラマ・作家の結城浩氏からのエール.
* バグがどうしても取れなくてイライラする時,なにもかもうまく行かない時,自分はプログラミングに向いてないと思う時,そういう時に読みましょう.わたしも,結城氏みたいなすごい人も,みんなそういう経験をしています.
* [教えてクン養成マニュアル](http://www.redout.net/data/osietekun.html)
* Don't be that guy(あんなやつになるなよ.)

View File

@@ -0,0 +1,238 @@
---
title: 任意長のタプルをサポートする SRTP 制約の書き方
description: F# の SRTP で任意長のタプル型を構築・操作する方法を解説
date: !!timestamp 2019-12-25T00:00:00.0000000+09:00
category: Programming
tags: ["F#", "SRTP"]
---
> この記事は [以前 Qiita に投稿した記事](https://qiita.com/cannorin/items/aaf6abb2a7c1bc7793d4) の転載です.
あんまり一般受けするようなネタが思いつかなかったのでSRTP をゴリゴリ書いてる人向けの記事になってしまいました.ごめんなさい……
FSharpPlus で現在開発中の,[型レベルリテラルでの依存型エミュレーション](https://github.com/fsprojects/FSharpPlus/pull/192) で用いた技法です.
## タプル型の内部表現
F# においてタプル型は,
* 7-tuple までは `System.Tuple` の1〜7個版を用いる
* 8-tuple 以上は ```Sytem.Tuple`8``` の8個目に8番目以降の要素をネストさせて入れるちょうど 8-tuple の場合は8番目は ```System.Tuple`1``` に入れる.
という内部表現になっている.例えば,
* `int*int*int` は ```System.Tuple`3[System.Int32,System.Int32,System.Int32]```
* `int*int*int*int*int*int*int*int` は ```System.Tuple`8[.., System.Tuple`2[System.Int32, System.Int32]]```
となる.単純な型レベルリストになっていないので SRTP で分解・構成するのは難しい.
しかも面倒なことにF# の型システム上ではタプルと ```System.Tuple``` による表現は **8-tuple 以上の時のみ** 区別される.つまり,以下のようになる:
```fsharp
> let (a,b,c) = System.Tuple<_,_,_>(1,2,3);;
val c : int = 3
val b : int = 2
val a : int = 1
> let (a,b,c,d,e,f,g,h) = System.Tuple<_,_,_,_,_,_,_,_>(1,2,3,4,5,6,7,System.Tuple<_>(8));;
let (a,b,c,d,e,f,g,h) = System.Tuple<_,_,_,_,_,_,_,_>(1,2,3,4,5,6,7,System.Tuple<_>(8));;
------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/home/alice/stdin(17,25): error FS0001: This expression was expected to have type
''a * 'b * 'c * 'd * 'e * 'f * 'g * 'h'
but here has type
'System.Tuple<int,int,int,int,int,int,int,System.Tuple<int>>'
```
つまり,```Sytem.Tuple`8``` で n-tuple (n>7) の内部表現を作っても,それを n-tuple 型として扱うことはできない.
## 黒魔術 `retype`
そこでundocumented な機能である inline IL を使って,内部的に同一であるような型を無理やり同一視する.
inline IL は `(# .. #)` で囲った中に直接 IL やその他の低レベル命令を書ける機能で,本来は標準ライブラリである `FSharp.Core` の内部でしか使うことができない.
```fsharp
#nowarn "0042"
let inline internal retype (x: 'T) : 'U =
(# "" x: 'U #)
```
この関数 `retype` は,`'T` 型の値 `x` を無理やり `'U` 型として解釈する.キャストを行うわけではない (=型の内部表現を変えるわけではない) のでOCaml の `Obj.magic` に近い挙動だ.
これを使ってやれば先ほどの例を動くようにできる.
```fsharp
> let (a:int,b:int,c:int,d:int,e:int,f:int,g:int,h:int) =
- retype <| System.Tuple<_,_,_,_,_,_,_,_>(1,2,3,4,5,6,7,System.Tuple<_>(8));;
val h : int = 8
val g : int = 7
val f : int = 6
val e : int = 5
val d : int = 4
val c : int = 3
val b : int = 2
val a : int = 1
```
ただし,`val inline internal retype : x:'T -> 'U` なので,変換先の型がわかっている必要があり,それを注釈で明示してあげないといけない.
## タプルを分解する
タプルを構成する方法はわかったので,分解もしてみよう.
まずn-tuple (n>7) 以上全てにマッチするオーバーロードを書くために,以下のヘルパー関数を導入する.
```fsharp
let inline whenNestedTuple (t: 't) =
(^t: (member Item1: 't1) t), (^t: (member Item2: 't2) t), (^t: (member Item3: 't3) t),
(^t: (member Item4: 't4) t), (^t: (member Item5: 't5) t), (^t: (member Item6: 't6) t),
(^t: (member Item7: 't7) t), (^t: (member Rest: 'tr) t)
// val inline whenNestedTuple :
// t: ^t -> 't1 * 't2 * 't3 * 't4 * 't5 * 't6 * 't7 * 'tr
// when ^t : (member get_Item1 : ^t -> 't1) and
// ^t : (member get_Item2 : ^t -> 't2) and
// ^t : (member get_Item3 : ^t -> 't3) and
// ^t : (member get_Item4 : ^t -> 't4) and
// ^t : (member get_Item5 : ^t -> 't5) and
// ^t : (member get_Item6 : ^t -> 't6) and
// ^t : (member get_Item7 : ^t -> 't7) and
// ^t : (member get_Rest : ^t -> 'tr)
```
これは,```System.Tuple`8``` には7番目の要素までに対応する `Item1` ~ `Item7` というプロパティと8番目の要素が入った `Rest` というプロパティがあることを利用して,それぞれを分解するための関数だ.
わざわざ member constraints を使って書いているのはSRTP は内部表現の型のみを見るので8-tuple でも 9-tuple でも使えるからだ.
```fsharp
> whenNestedTuple (1,2,3,4,5,6,7,8,9);;
val it : int * int * int * int * int * int * int * (int * int) =
(1, 2, 3, 4, 5, 6, 7, (8, 9))
```
あとは 8-tuple 以上のケースを `whenNestedTuple` を使ったオーバーロードにマッチさせて,いつものように SRTP を書いていくだけだ.
## 例1: タプルの arity を数える
```fsharp
type CountTuple =
static member inline Invoke xs : int =
let inline call_2 (a: ^a, b: ^b) = ((^a or ^b) : (static member CountTuple: _*_ -> _) b, a)
let inline call (a: 'a, b: 'b) = call_2 (a, b)
call (Unchecked.defaultof<CountTuple>, xs)
static member inline CountTuple (t: 't, ct: ^CountTuple) =
let _,_,_,_,_,_,_,tr : _*_*_*_*_*_*_* ^TR = whenNestedTuple t
7 + ((^TR or ^CountTuple): (static member CountTuple: _*_->_) tr,ct)
static member inline CountTuple (_: Tuple<_>, _: CountTuple) = 1
static member inline CountTuple ((_, _), _: CountTuple) = 2
static member inline CountTuple ((_, _, _), _: CountTuple) = 3
static member inline CountTuple ((_, _, _, _), _: CountTuple) = 4
static member inline CountTuple ((_, _, _, _, _), _: CountTuple) = 5
static member inline CountTuple ((_, _, _, _, _, _), _: CountTuple) = 6
static member inline CountTuple ((_, _, _, _, _, _, _), _: CountTuple) = 7
```
使用例:
```fsharp
> CountTuple.Invoke (1,2,3,4,5);;
val it : int = 5
> CountTuple.Invoke (1,2,3,4,5,6,7,8,9,0,1,2);;
val it : int = 12
```
## 例2: 同じ型のみのタプルをリストに変換する
```fsharp
type TupleToList =
static member inline Invoke xs : 'x list =
let inline call_2 (a: ^a, b: ^b) = ((^a or ^b) : (static member TupleToList: _*_ -> _) b, a)
let inline call (a: 'a, b: 'b) = call_2 (a, b)
call (Unchecked.defaultof<TupleToList>, xs)
static member inline TupleToList (t: 't, ct: ^TupleToList) =
let t1,t2,t3,t4,t5,t6,t7,tr : _*_*_*_*_*_*_* ^TR = whenNestedTuple t
t1::t2::t3::t4::t5::t6::t7::((^TR or ^TupleToList): (static member TupleToList: _*_->_) tr,ct)
static member inline TupleToList (x: Tuple<_>, _: TupleToList) = [x.Item1]
static member inline TupleToList ((x1,x2), _: TupleToList) = [x1;x2]
static member inline TupleToList ((x1,x2,x3), _: TupleToList) = [x1;x2;x3]
static member inline TupleToList ((x1,x2,x3,x4), _: TupleToList) = [x1;x2;x3;x4]
static member inline TupleToList ((x1,x2,x3,x4,x5), _: TupleToList) = [x1;x2;x3;x4;x5]
static member inline TupleToList ((x1,x2,x3,x4,x5,x6), _: TupleToList) = [x1;x2;x3;x4;x5;x6]
static member inline TupleToList ((x1,x2,x3,x4,x5,x6,x7), _: TupleToList) = [x1;x2;x3;x4;x5;x6;x7]
```
使用例:
```fsharp
> TupleToList.Invoke (1,2,3);;
val it : int list = [1; 2; 3]
> TupleToList.Invoke (1,2,3,4,5,6,7,8,9);;
val it : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9]
```
## 例3: 型レベル自然数に応じた arity のタプルを作る
`retype` を使ってタプルを構成する例.
```fsharp
type S<'a> = S of 'a with static member Succ (S n) = S (S n)
type Z = Z with static member Succ Z = S Z
let inline succ (n: ^n) = (^n: (static member Succ:_->_) n)
type ArrayToTuple =
static member inline Invoke (xs: 'x[], n, ?index) =
let inline call_2 (_a: ^a, b: ^b) = ((^a or ^b): (static member ArrayToTuple:_*_*_->_) xs,b,defaultArg index 0)
let inline call (a: 'a, b: 'b) = call_2 (a, b)
call (Unchecked.defaultof<ArrayToTuple>, n) |> retype
static member inline ArrayToTuple (xs:_[],S(S(S(S(S(S(S(S(n)))))))), i) =
Tuple<_,_,_,_,_,_,_,_>(
xs.[i],xs.[i+1],xs.[i+2],xs.[i+3],xs.[i+4],xs.[i+5],xs.[i+6],
ArrayToTuple.Invoke(xs,succ n,i+7) // ここで S n とすると "早すぎるオーバーロード解決" が起こってしまう
)
static member inline ArrayToTuple (xs:_[], S Z, i) = Tuple<_>(xs.[i])
static member inline ArrayToTuple (xs:_[], S (S Z), i) = (xs.[i], xs.[i+1])
static member inline ArrayToTuple (xs:_[], S (S (S Z)), i) = (xs.[i], xs.[i+1], xs.[i+2])
static member inline ArrayToTuple (xs:_[], S (S (S (S Z))), i) = (xs.[i], xs.[i+1], xs.[i+2], xs.[i+3])
static member inline ArrayToTuple (xs:_[], S (S (S (S (S Z)))), i) = (xs.[i], xs.[i+1], xs.[i+2], xs.[i+3], xs.[i+4])
static member inline ArrayToTuple (xs:_[], S (S (S (S (S (S Z))))), i) = (xs.[i], xs.[i+1], xs.[i+2], xs.[i+3], xs.[i+4], xs.[i+5])
static member inline ArrayToTuple (xs:_[], S (S (S (S (S (S (S Z)))))), i) = (xs.[i], xs.[i+1], xs.[i+2], xs.[i+3], xs.[i+4], xs.[i+5], xs.[i+6])
```
使用例:
```fsharp
> let (a:int,b:int,c:int) = ArrayToTuple.Invoke ([|1;2;3|], S(S(S(Z))));;
val c : int = 3
val b : int = 2
val a : int = 1
> let (a:int,b:int,c:int,d:int,e:int,f:int,g:int,h:int,i:int,j:int) =
- ArrayToTuple.Invoke ([|1;2;3;4;5;6;7;8;9;10|], S(S(S(S(S(S(S(S(S(S(Z)))))))))));;
val j : int = 10
val i : int = 9
val h : int = 8
val g : int = 7
val f : int = 6
val e : int = 5
val d : int = 4
val c : int = 3
val b : int = 2
val a : int = 1
```
## はい
よかったですね

View File

@@ -0,0 +1,393 @@
---
title: F# で型クラス 2020
description: そろそろ F# での型クラスの実用的な作り方について書いておく
date: !!timestamp 2020-12-17T19:00:00.0000000+09:00
category: Programming
tags: ["F#", "SRTP"]
---
この記事は [F# Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fsharp) の12日目の記事です投稿が遅れたのは [Cyberpunk 2077](https://www.cyberpunk.net/) のせいであって,私のせいではありません.
昔このブログで [F# で型クラスを実現する方法](https://7colou.red/blog/2018/02-14-fsharp-typeclasses/index.html) について書きましたがFSharpPlus のメンテナになってからより実用的な手法を学んだので,改めて解説をしようというわけです.
この記事では SRTP 自体の解説はしません.
## 基本的な事項
まず,この手法は Haskell などで一般的な型クラスを **そのまま** F# 上で再現するものではない.具体的には,関数を一つだけ含む型クラスしか作れない.それでは複数の関数を含む型クラス[^1]はどうするのかというと,インスタンス側で複数の型クラスを実装してあげればよいので,特に困ることはない.
[^1]: Monad とか
前回紹介した手法では型クラスに対応したデータ型を作ってそこに関数を格納したが,関数呼び出しにオーバーヘッドが発生してしまっていた.この手法では関数呼び出しは全てインライン展開されるので,パフォーマンスへの影響は最小限になる.
またこの手法では型クラス同士の継承も自然に表現することができる例えばMonad を実装した型を自動的に Functor や Applicative にするということが可能であるFSharpPlus では[頻出する型クラスが一通り実装されている](https://fsprojects.github.io/FSharpPlus/abstractions.html)のでチェックしてみてほしい[^2]
[^2]: この記事の手法で実装された Monad や Applicative といった型クラスを使いたいだけなら,自分で作らずに FSharpPlus をインストールしてしまえばいいということでもある.
この手法の欠点は例えばモナドであれば「モナドである」ということを制約として表現する方法がなく「bind と return を実装している」とするしかないことだ.とはいえ SRTP 制約自体がとても手書きできたものではなく,書かずにコンパイラの推論に任せることが多いので,このことで困ることはあまりないだろう.
また前回の手法と同様にorphan instances を作ることはできない.
## SRTP によるオーバーロード解決の基本的な考え方
SRTP は型に対するパターンマッチを実現する.例えば,次のような型があるとする.
```fsharp
type S<'n> = S of 'n
type Z = Z
```
これを使えば型レベル自然数[^3]が表現できる例えば3は `S<S<S<Z>>>` である.
[^3]: これについての話はクリスマスの記事に.
型レベル自然数同士の足し算はどうすればいいだろうか? SRTPを使って型レベルのパターンマッチを行い再帰計算をしたいときは再帰的にオーバーロード解決を起こすようにすればよい
```fsharp
let inline ( +^ ) (x: ^X) (y: ^Y) =
(^X: (static member ( +^ ): _*_->_) x,y)
type S<'n> with
static member inline ( +^ ) (S x, y) = S (x +^ y)
type Z with
static member inline ( +^ ) (Z, y) = y
let three = S (S Z) + S Z /// S<S<S<Z>>>
```
今回は再帰呼び出しまでは行わないが,上の例とはまた違った複雑さをはらんでいる.それは orphan instances を作れないこと,つまり型クラスを定義しているモジュールの中で既存の型をインスタンス化しておく必要があることに由来している.
## 用語の導入
ある型クラスの関数 `foo` に対して次の2つの型を導入したい
* `FooImpl`
- 型クラス実装時に用意する,オーバーロード解決の対象になる型.
- 既存の型をインスタンス化するためのメソッドも,この型に定義する.
- この記事では,この種の型を **Impl型** と呼びたい.
* `Foo`
- メソッド `Foo.Invoke` を呼び出すことで `foo` を使う.
- 別になくてもいいのだが,実用上は上の `FooImpl` 型と合体させてしまうのが扱いやすい.
## インスタンス化する既存の型の取り扱い方
ユーザ定義型は SRTP で受け取り,オーバーロード解決を起こさせればいいだけだが,既存の型をインスタンス化したい場合は複数の方法がある.
これはあなたが実装したいモナド(の関数)の種類によって変わってくる.
なお,「オーバーロード解決をかけるメソッドのシグネチャ」とは,
```fsharp
(^T: (static member ...: ...) ...)
```
の構文を使ってオーバーロード解決をかけたいメソッドのシグネチャのことであり,「ユーザ定義型で実装させたいメソッドのシグネチャ」とは,例えば自作型 `A` を作るとして,
```fsharp
type A = ... with
static member ... (...) =
...
```
として型クラスをインスタンス化する際に書きたいメソッドのシグネチャのことである.
### 1. オーバーロード解決をかけるメソッドのシグネチャと,ユーザ定義型で実装させたいメソッドのシグネチャが同じでよい場合
このケースは単純である.
1. 既存の型用の解決対象のメソッドを含む Impl型
2. ユーザ定義型(自分自身で解決対象のメソッドを持つ)
の2択でオーバーロード解決をかければよいオーバーロード解決の観点からはこの2つは同じ優先順位にある
具体的には,次のような例である.
```fsharp
type BindImpl =
static member ( >>= ) (m: _ option, f) = Option.bind f m
static member ( >>= ) (m: _ list, f) = List.collect f m
type Bind =
static member inline Invoke (value: ^Ma, binder: 'a -> ^Mb) : ^Mb =
// このヘルパー関数は書き方によって警告が出たり出なかったりする
// 警告が出るのはオーバーロード解決が早く起こりすぎてしまうのが原因だが,
// オーバーロード解決を遅延させる一般的な方法はよくわからない
let inline call (_impl: ^impl, m: ^m, _r: ^r, f) =
((^impl or ^m or ^r): (static member (>>=): _*_->_) m,f)
call (Unchecked.defaultof<BindImpl>, value, Unchecked.defaultof< ^Mb >, binder)
let inline (>>=) m f = Bind.Invoke (m, f)
type M<'t> = M of 't with
static member (>>=) (M x, f) : M<_> = f x
let m1 = Some 2 >>= fun x -> Some (x + 1)
let m2 = [1;2;3] >>= fun x -> [x; x+1; x+2]
let m3 = M 2 >>= fun x -> M (x + 1)
```
ここで起こっているオーバーロード解決は,ユーザ定義型 or `BindImpl` の2択である
[F# で型クラス](https://7colou.red/blog/2018/02-14-fsharp-typeclasses/index.html) でやったことがわかっていれば
特に理解に苦しむところはないと思う.
もしあなたが実装したいモナド(の関数)がこの類ならば,おめでとう.話はここでおしまいである.しかし実際は,これで
綺麗に実装できるのは `>>=` くらいしかない.
### 2. オーバーロード解決をかけるメソッドのシグネチャと,ユーザ定義型で実装させたいメソッドのシグネチャを別にしなければならない場合
ほとんどの関数がこのケースに該当する.このケースに該当するのは,オーバーロード解決において本来の引数以外にダミー引数を加えて表現しなければいけない情報が入っているときで,具体的には以下の通りである.
1. 戻り値の型に応じて呼び出すメソッドが変わるとき(例: `return`
- 戻り値の型を格納するダミー引数を加えないとオーバーロード解決ができない.
2. 他の型クラスと継承関係にあるとき (例: `fmap`)
- ユーザ定義型が `>>=``return` だけを実装している場合は,それらを使いたい.
- しかし,ユーザ定義型が明示的に `fmap` を定義している場合は,`>>=``return` を無視してそちらを使いたい.
- オーバーロード解決の優先順位の情報をダミー引数で持つ必要がある.
3. 特定のクラス・インターフェースを継承していれば使えるようにしたいとき (例: `fold`)
- ユーザ定義型が `seq` を継承している場合,`Seq.fold` を使いたい.
- しかし,ユーザ定義型が明示的に `fold` を実装している場合,`seq<'t>` を継承していたとしても無視してそれを使いたい.
- オーバーロード解決の優先順位の情報をダミー引数で持つ必要がある.
ユーザ定義型で実装するメソッドのシグネチャには上で述べたダミー引数を含めたくない[^4].そのため,オーバーロード解決の対象になるメソッドとユーザ定義型で実装するメソッドのシグネチャが異なることになる.
[^4]: 含めてもいいが,インスタンス化が冗長になってしんどいし,オーバーロード解決が壊れかねない.
もっと言うと 1. はオーバーロード解決の優先順位の問題でもある.どういうことかというと,例えば `return` を素朴に実装しようとすると以下のようになるだろう.
```fsharp
type ReturnImpl =
// 既存の型用
static member Return (_: 'a option, x: 'a) = Some x
static member Return (_: 'a list, x: 'a) = [x]
// ユーザ定義型用
static member inline Return (_: ^Ma, x: 'a) = (^Ma: (static member Return: 'a -> ^Ma) x)
type Return =
static member inline Invoke (x: 'x) : ^Ma =
let inline call_2 (_impl: ^a, output: ^b, x) = ((^a or ^b): (static member Return: _*_->_) output, x)
let inline call (impl: 'a, output: 'b, x) = call_2 (impl, output, x)
call (Unchecked.defaultof<ReturnImpl>, Unchecked.defaultof< ^Ma >, x)
let inline return_ x = Return.Invoke x
```
しかしこれでは `option``list``Return` メソッドが呼ばれてくれない.なぜそうなるかというと,この場合のオーバーロード解決時の型の候補は Impl型 である `ReturmImpl` 一択であり,コンパイラは次に `ReturnImpl` に含まれる3つの `Return` メソッドを同じ優先順位で解決しようとする.
ここで SRTP はあくまで型パラメータなのでコンパイラは3つ目のオーバーロードが一番汎用性が高いと判断してそれを使ってしまうしかし`list` 型や `option` 型それ自身は `Return` メソッドを持たないため,制約解決に失敗する.
コンパイラが特定のオーバーロードを選んだ結果として SRTP の制約解決に失敗した場合は,オーバーロード解決にフォールバックせずにそのままエラーを吐き,型付けに失敗してしまうのだ.
この問題には別の解決方法もあるがSRTPを含むメソッドの優先順位をそうでないメソッドよりも下げることでも解決できる
というわけで,まずはオーバーロード解決の優先順位付けについて解説しようと思う.
## オーバーロード解決の優先順位付け
幸いなことに,ある方法を使えば,オーバーロード解決に対して優先順位を付けることができる.その方法とは,
1. Impl型を他の型の部分型にしておく
2. オーバーロード解決対象になるメソッドのシグネチャにImpl型をダミー引数として含めておく
3. オーバーロード解決の優先順位が低いメソッドはシグネチャ中のImpl型のダミー引数の型を上位型に変えるより優先順位が低いメソッドはさらに上位の型を使う
というものであるF# の制約解決器はどうやら「オーバーロードが見つからなかった場合,引数の型を上位型にしたものを探す」という挙動になっているらしく,後に解決して欲しいメソッドほど上位の型になるようなダミー引数を用意してあげることで優先順位を下げることができる.
次のようなヘルパー型を用意しておく.
```fsharp
type Default1 = class inherit Default2 end
and Default2 = class inherit Default3 end
and Default3 = class end
```
これをどう使うかというと,
* オーバーロード解決の対象になる型(ここでは `FooImpl` とする)は `Default1` 型を継承するようにする.
* オーバーロード解決対象のメソッドに一つダミー引数を増やす.
- オーバーロード解決の優先度が一番高いメソッドは,そのダミー引数の型を `FooImpl` にする.
- 優先度が次に高いメソッドは,そのダミー引数の型を `Default1` にする.
- 優先度がその次に高いメソッドは,そのダミー引数の型を `Default2` にする.
- ...
というようになる.もし `N+1` 段階の優先順位を付けたい場合, `DefaultN` 型まで作る必要がある.
## 優先度付きのメソッドの配置ルール
さてF# コンパイラが意図通りにオーバーロードを解決してくれるようにするために,守るべきルールがある.
1. SRTP を含むオーバーロードは,同じ優先順位の中には一つしかないようにする.
- 例えば `fmap` の場合,ユーザ定義の `fmap` を呼ぶためのメソッドと,ユーザ定義の `>>=``return` を使うメソッドは,別の優先順位に置かなければならない.
- これはF# は2つのメソッドのシグネチャの等価性を判断する時型変数に付いている制約を考慮しないからである全く異なるメンバー制約が付いた SRTP であっても,ただの型変数に変えた時に同じになるならば,同一のシグネチャと判断される.
- F# においては同一のシグネチャを持つメソッドが複数存在することは許されないので,コンパイルエラーになる.
2. 同じ優先順位内に以下に挙げる種類のオーバーロードを全く含まないか2つ以上を含んでいる必要がある[^5]
* オーバーロード解決に関係する引数の型が,インターフェースもしくは継承が可能なクラスである.
* オーバーロード解決に関係する引数の型が型変数現実的にはSRTPである
[^5]: これに関しては,優先順位が一番高い場合は絶対に従う必要があるようだが,それ以外の場合ではなくでも問題ないことが多々ある.怖い.
2.についてはF# コンパイラ的には部分型制約も SRTP 制約も「解かなければいけない」という点で同じであるので,
オーバーロード解決の時点では同じ種類の物体と認識しているのだと思う.
F# コンパイラはこの種のオーバーロードが1個のみ存在しているならば無条件でマッチしようと試みて失敗した場合他の候補を無視してコンパイルエラーを吐く
しかしこの種のオーバーロードが2個以上存在しかつ現在解決中の型がそのどちらにもマッチしないとき両方とも無視してオーバーロード候補の探索を進める性質があるようだ
もしインスタンス化する既存の型に継承可能なクラス[^6]やインターフェースが1つもない場合ダミーのオーバーロードが2つ必要になるそれに用いるために以下のダミー型を用意しておくとよい
[^6]: `option``list` などは,内部的にはクラスで実装されているが,継承可能ではない.一方,`seq` はインターフェース.
```fsharp
type Dummy1<'t>(x: 't) = class member val Value1 = x end
type Dummy2<'t>(x: 't) = class member val Value2 = x end
```
ちなみに SRTP を使ったオーバーロードとダミーのインターフェースを使ったオーバーロードを同じ階層に置いても意図通り動作するようだ.もし不安ならば,
* SRTP正確にはメンバ制約が付いている型変数の制約を `when ^t: null and ^t: struct` に変える
* 戻り値の arity を変える戻り値がただの値ならば1変数関数にするなど
をしたオーバーロードを作ってあげればよい.これは
* オーバーロード解決の候補になる(引数の数が同じなため)
* 元の SRTP を使ったオーバーロードとシグネチャが異なる(戻り値の arity が異なるため)
* 絶対にマッチすることがない(ありえない制約を要求しているため)
ので,うまくダミーのオーバーロードとして機能する.例えば,
```fsharp
static member inline Return (_: ReturnImpl, _: ^Ma, x: 'a) = (^Ma: (static member Return: 'a -> ^Ma) x)
```
に対しては,
```fsharp
static member inline Return (_: ReturnImpl, _: ^Ma when ^Ma: struct and ^Ma: null, _) = id
```
としてあげるとよい.戻り値が `id` なので arity が異なることに注目.
## 実例: Foldable
例として `fold` を実装してみよう. `fold` に期待する挙動は以下のとおりだ.
* 既存の型として `list`, `option`, `[]`, `seq` をサポートしたい.
* ユーザ定義型がメンバーとして `Fold` を持っている場合,それを優先して使いたい.
* それ以外の場合,`seq` を継承しているなら `Seq.fold` を使いたい.
よって,メソッドの優先順位は次のようになるだろう.
* 優先順位0 (`Fold`):
- `list`
- `option`
- `[]`
* 優先順位1 (`Default1`):
- ユーザ定義型 (SRTP)
* 優先順位2 (`Default2`)
- `seq` (インターフェース)
ルールに従い優先順位1と優先順位2に一個ずつダミーのオーバーロードを追加することにする
よって,`fold` の定義は以下のようになる[^7]
[^7]: 面倒なので `FoldImpl``Fold` を分けずに一つの型で実装している.
```fsharp
type Fold =
inherit Default1
// 優先順位0
static member Fold (f, state, xs: _ list, [<Optional>]_impl: Fold) = List.fold f state xs
static member Fold (f, state, xs: _[], [<Optional>]_impl: Fold) = Array.fold f state xs
static member Fold (f, state, xs: _ option, [<Optional>]_impl: Fold) = Option.fold f state xs
// 優先順位1
static member inline Fold (f: 's -> 'a -> 's, state: 's, xs: 'Fa, [<Optional>]_impl: Default1) = (^Fa: (static member Fold: _ * _ * ^Fa -> ^s) f,state,xs)
static member Fold (f, state, xs: Dummy1<_>, _impl: Default1) = f state xs.Value1
// 優先順位2
static member Fold (f, state, xs: _ seq, [<Optional>]_impl: Default2) = printfn "using Seq.fold"; Seq.fold f state xs
static member Fold (f, state, xs: Dummy1<_>, _impl: Default2) = f state xs.Value1
static member inline Invoke (folder: 'State -> 'T -> 'State) (state: 'State) (foldable: 'FoldableT) : 'State =
let inline call_2 (impl: ^a, input: ^b, f, x) = ((^a or ^b): (static member Fold: _*_*_*_ -> _) f,x,input,impl)
let inline call (impl: 'a, input: 'b, f, x) = call_2 (impl, input, f, x)
call (Unchecked.defaultof<Fold>, foldable, folder, state)
let inline fold (f: 'State -> 'T -> 'State) (state: 'State) (foldable: 'FoldableT) : 'State = Fold.Invoke f state foldable
```
これを以下の例を使ってテストしてみる.
```fsharp
type C<'t> = { item: 't list } with
interface System.Collections.Generic.IEnumerable<'t> with
member this.GetEnumerator() = (this.item :> _ seq).GetEnumerator()
interface System.Collections.IEnumerable with
member this.GetEnumerator() = (this.item :> _ seq).GetEnumerator() :> _
static member Fold (f, state, x: C<_>) =
printfn "using C.Fold"
List.fold f state x.item
// list, [], option
let p1 = fold (fun state x -> x :: state) [] [ 1; 2; 3 ]
let p2 = fold (fun state x -> x :: state) [] [| 1; 2; 3 |]
let p3 = fold (fun state x -> x :: state) [] (Some 42)
// seq を継承している型
let p4 = fold (fun state x -> x :: state) [] (Set.ofList [ 1; 2; 3 ])
// seq を継承しFold も定義されている型
let p5 = fold (fun state x -> x :: state) [] { item = [ 1; 2; 3 ] }
```
なお,デバッグのために `Seq.fold``C.fold` には `printfn` を仕込んである.これは実行時に
```
using Seq.fold
using C.fold
```
を出力するので,正しいオーバーロードが選ばれていることがわかる.
## で,結局モナドはどう実装すればいいの
わかりやすいように実装すると,以下のようになるだろう.
```fsharp
type BindImpl =
static member ( >>= ) (m: _ option, f) = Option.bind f m
static member ( >>= ) (m: _ list, f) = List.collect f m
static member ( >>= ) (m: _ seq, f) = Seq.collect f m
static member ( >>= ) (m: Dummy1<_>, f) = f m.Value1
type Bind =
static member inline Invoke (value: ^Ma, binder: 'a -> ^Mb) : ^Mb =
let inline call (_impl: ^impl, m: ^m, _r: ^r, f) =
((^impl or ^m or ^r): (static member (>>=): _*_->_) m,f)
call (Unchecked.defaultof<BindImpl>, value, Unchecked.defaultof< ^Mb >, binder)
let inline (>>=) m f = Bind.Invoke (m, f)
type ReturnImpl =
inherit Default1
static member Return (_: Default1, _: 'a option, x: 'a) = Some x
static member Return (_: Default1, _: 'a list, x: 'a) = [x]
static member Return (_: Default1, _: 'a seq , x: 'a) = Seq.singleton x
static member Return (_: Default1, _: Dummy1<'a>, x: 'a) = new Dummy1<_>(x)
static member inline Return (_: ReturnImpl, _: ^Ma, x: 'a) = (^Ma: (static member Return: 'a -> ^Ma) x)
static member Return (_: ReturnImpl, _: Dummy1<'a>, x: 'a) = new Dummy1<_>(x)
type Return =
static member inline Invoke (x: 'x) : ^Ma =
let inline call_2 (impl: ^a, output: ^b, x) = ((^a or ^b): (static member Return: _*_*_->_) impl,output,x)
let inline call (impl: 'a, output: 'b, x) = call_2 (impl, output, x)
call (Unchecked.defaultof<ReturnImpl>, Unchecked.defaultof< ^Ma >, x)
let inline return_ x = Return.Invoke x
type M<'t> = M of 't with
static member (>>=) (M x, f) : M<_> = f x
static member Return x = M x
let m1 = Some 2 >>= fun x -> return_ (x + 1) // int option
let m2 = [1;2;3] >>= fun x -> return_ (x + 1) // int list
let m3 = M 2 >>= fun x -> return_ (x + 1) // M<int>
```
## はい
よかったですね

View File

@@ -0,0 +1,521 @@
---
title: FSharpPlus に型安全な固定長リストが実装されるまでの話
description: SRTP と型プロバイダを使った黒魔術が盛りだくさんです
date: !!timestamp 2020-12-25T00:00:00.0000000+09:00
category: Programming
tags: ["F#", "SRTP"]
---
この記事は [F# Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fsharp) の25日目の記事です
今年の4月に[型システム祭りオンライン](https://opt.connpass.com/event/169724/)で発表した内容をもとにしています.スライドの内容に比べて大幅に加筆されていますが,忙しくて読む暇がない!という方向けに当該スライドも貼っておきます.
<script async class="speakerdeck-embed" data-slide="1" data-id="cdaf450125fa4655b57870cd3641d8a5" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>
----
## FSharpPlus と私について
私です.ブログ記事には書き忘れたんですが去年の夏ぐらいから [FSharpPlus](https://github.com/fsprojects/FSharpPlus) のメンテナをやっています[^ionide]
[^ionide]: [Ionide のとき](https://7colou.red/blog/2019/ionide-vim.html) は記事にしたのにね
[FSharpPlus](https://github.com/fsprojects/FSharpPlus) というのはF# の標準ライブラリの拡張版として開発されているライブラリで,
* 標準ライブラリに足りていない関数(`String.toLower` など)
* 対応する型ならなんでも使える,ジェネリックな関数と演算子(配列にもリストにも使える `map` など)
* Haskell や Scala でみられるような型クラスと,さまざまなモナド
* `NonEmptyList` などの便利なデータ型
などを含んでいますFSharpPlus をプロジェクトに追加するだけでF# のかゆいところに手が届くだけでなく,他の言語でみられるような強力な抽象化機能を使えるようになるわけです.しかも他の言語の概念・用語をそのまま輸入するのではなく,既存の F# のスタイルとなるべく近くなるように工夫されています.
> F#+ is a base library that aims to take F# to the next level of functional programming.
>
> What if we imagine F# as more than it is?
>
> F#+ builds upon FSharp, using generic programming techniques to help you avoid boiler plate code. However, by naming conventions and signatures it can be seen to 'enhance' rather than 'replace' existing patterns as much as possible.
> F#+ は F# でより高度な関数型プログラミングをすることを目標とした標準ライブラリです.
>
> もし F# が現状以上に強力な言語だったとしたら……?
>
> F#+ を使えば F# の言語機能が拡張されてジェネリックプログラミングの手法によって冗長なコードを削減することができるようになりますしかしF# 既存のプログラミングパターンを「置き換える」のではなく,むしろ「強化する」ものと言えるように,できる限り命名規則や関数のシグネチャを工夫しています.
今回は去年の9月くらいから実装を開始し今年の4月にマージされた[型レベルリテラルと自然数依存のベクトル・行列型](https://github.com/fsprojects/FSharpPlus/pull/192) の話です.
## 背景
ある日のこと,あるユーザが [FSharpPlus に型安全な固定長ベクトル・行列が欲しい](https://github.com/fsprojects/FSharpPlus/issues/178) という feature request を投げてきた.
ここで言う「型安全」とは,境界外アクセスや(行列演算の)次元の不一致などのエラーを実行時例外としてではなく,コンパイル時に検出してコンパイルエラーにできるということだ.
この種の安全性を保証するのはとても難しいことで,具体的には
1. ベクトルの長さや行列の次元の数値をコンパイル時に扱う(加減乗除などの演算ができるともっとよい)
2. 長さや次元の情報を使って,境界外アクセスなどを静的に検出してコンパイルエラーを出す
の2つができなければならない
これらを自然に実現できる言語はだいたい[依存型や篩型](https://7colou.red/blog/2018/07-07-difference/index.html)などの強力な型システムを搭載している.当然 F# はそれらをサポートしていないのでF# の言語機能の範囲内でなんとかしなければならない.
さらに FSharpPlus は標準ライブラリの拡張版として作られているものなのでF# ユーザにとって馴染みのあるような API を提供する必要がある.例え裏でどんな黒魔術をしていようとも,ユーザを混乱させないためにその事実をなるべく隠して,扱いやすい形にしなければならないわけだ.
この feature request はたまたまその方面に明るかった私が担当することになったそしてHaskell の同様のライブラリでも採用されている **型レベルリテラル****型レベル計算** を用いて実装することにした.つまり,
1. ベクトル長や行列の次元は型レベルリテラルで管理する(加減乗除は型レベル計算で行う)
2. 境界外アクセスなどの検出は型レベル計算で静的アサーションを行ってチェックする
ということになる.
## 実現できたこと
先に何が実現できたのかについて紹介しておこう.
* 型レベル自然数・ブール値と,それらに対する演算
* 型安全かつ実行時の効率低下を最小限に抑えた固定長ベクトル・行列
* それらを使うための簡潔で直観的な記法・API
* 上記全てをライブラリとして実装(言語拡張ではない!)
百聞は一見にしかずということで,実際に固定長ベクトルを使ったコードをお見せしよう.
```fsharp
let v = vector (1, 2, 3)
// val v: Vector<int, S<S<S<Z>>>> = [| 1; 2; 3 |]
let a = v |> Vector.Get<2>.Invoke
// val a: int = 3
let b = v |> Vector.Get<3>.Invoke
// error FS0001: この式に必要な型は
// 'True'
// ですが、ここでは次の型が指定されています
// 'False'
```
いい感じの記法でベクトルを定義すると勝手に型とベクトルの長さが推論されて,いい感じに要素アクセスもできて,境界外ならコンパイルエラーになってくれる[^vector-err]
[^vector-err]: コンパイルメッセージはちょっとわかりにくいかもしれない.要は静的アサーションの条件式が `False` を返したというエラーだ.もっと改善できないかは調査中
行列はどうだろう.
```fsharp
let m1 =
matrix (
(1, 2, 3),
(4, 5, 6)
)
// val m1: Matrix<int, S<S<Z>>, S<S<S<Z>>>>
// = [[1; 2; 3]
// [4; 5; 6]]
let m2 =
matrix (
(1, 0),
(0, 0),
(0, 1)
)
// val m2: Matrix<int, S<S<S<Z>>>, S<S<Z>>>
// = [[1; 0]
// [0; 0]
// [0; 1]]
let m3 = m1 |*| m2
// val m3: Matrix<int, S<S<Z>>, S<S<Z>>>
// = [[1; 3]
// [4; 6]]
let m4 = m1 |*| m3
// error FS0001: 型が一致しません。
// 'Matrix<int,S<S<S<Z>>>,'a>'
// という指定が必要ですが、
// 'Matrix<int,S<S<Z>>,S<S<Z>>>'
// が指定されました。型 'S<Z>' は型 'Z' と一致しません
```
こちらもいい感じ.ちなみに `|*|` はベクトル同士の積を計算する演算子だ.
実用重視ということで,[`Vector`](http://fsprojects.github.io/FSharpPlus/reference/fsharpplus-data-vector.html) モジュールと [`Matrix`](http://fsprojects.github.io/FSharpPlus/reference/fsharpplus-data-matrix.html) モジュールには,コレクションとして当然用意されてほしい `map` などの関数がF# の標準ライブラリと同じような命名規則で用意されている.
さらに `Functor``Applicative` などの FSharpPlus 組み込みの型クラスもデフォルトで対応している.気になったあなたは今すぐ FSharpPlus のプレリリース版をインストールして固定長ベクトルと行列を使ってみよう!
[`dotnet add package FSharpPlus --prerelease`](https://www.nuget.org/packages/FSharpPlus)
ひととおり紹介が済んだところで,ここからはこれらをどのように実現したのかを順番に解説していく.
## 型レベル自然数とそれに対する演算
ベクトル長や行列の次元を型で管理するためには,型レベル自然数が必要である.
ペアノ自然数を使って型レベルで自然数を作ると,まぁ次のようになるだろう.
```fsharp
type S<'n> = S of 'n
type Z = Z
let three = S (S (S Z))
// val three: S<S<S<Z>>> = S (S (S Z))
```
表現自体は簡単なのだが,問題は演算をどのように実装するかだ.例えば,ペアノ算術上での加算は次のように定義されている.
$$
\begin{aligned}
0 + x &= x \\
S(x) + y &= S (x + y) \\
\end{aligned}
$$
この加算を上の型レベル自然数に対して定義するには,どうにかして F# 上で型を再帰的に操作してあげる必要があるF# で型を操作する(ある型から別の型を作る)には関数呼び出しを使うしかない(引数の型が入力,戻り値の型が出力).この場合は引数の型に対するパターンマッチを行い,その結果に応じて戻り値の型を変えるような関数を作る必要がある.
型に対するパターンマッチとは,すなわちオーバーロード解決である.
F# では `inline` キーワードと SRTP を使うことでオーバーロードされた関数・演算子を定義することができる例えばF# において `+` という演算子は,右辺か左辺の型が `static member` として `+` を定義しているかどうかをコンパイル時にチェックして,解決先の実装を呼び出すコードをインライン展開するようになっている.
```fsharp
let inline (+) (x: ^T) (y: ^U) : ^V =
((^T or ^U): (static member (+): ^T * ^U -> ^V) x,y)
let a = 1 + 2
// int 型は組み込みで static member (+) を定義しているので使える
// val a: int = 3
type MyType = { value: int } with
static member (+) (x: MyType, y: MyType) =
{ value = x.value + y.value }
let b = { value = 1 } + { value = 2 }
// ユーザ定義の MyType 型も static member (+) を定義してあるので使える
// val b: MyType = { value = 3 }
```
では,与えられた型レベル自然数の加算を行う演算子 `+^` を定義しよう.
上の例と同様に,型 `S<'n>``Z` それぞれに対応する `static member` を追加する.
再帰的に計算を行うには,`static member` の内部で `+^` を使って,再帰的にオーバーロード解決を起こさせてやればよい.
そういうわけで,型レベル自然数同士の加算は定義に忠実に素直に書けてしまう.
```fsharp
let inline ( +^ ) (x: ^X) (y: ^Y) =
(^X: (static member ( +^ ): _*_->_) x,y)
type S<'n> with
static member inline ( +^ ) (S x, y) = S (x +^ y)
type Z with
static member inline ( +^ ) (Z, y) = y
let three = S (S Z) + S Z
/// val three: S<S<S<Z>>> = S (S (S Z))
```
実は加算以外は一筋縄では行かなかったりするのだが,それらに用いたテクニックについては明文化がなかなか難しいので割愛したい.気になる方は[実装](https://github.com/fsprojects/FSharpPlus/blob/master/src/FSharpPlus/TypeLevel/TypeNat.fs)を見てみてほしい.
## 型レベルブール値を用いた静的アサーション
さて,我々は型レベル自然数同士を比較して静的アサーションをしたいので,型レベルブール値とそれに対する演算も作ってあげる必要がある.ブール値は自然数と違って再帰的な構造になっていないので,実装は比較的簡単である.
```fsharp
type True = True
type False = False
let inline ( &&^ ) (x: ^X) (y: ^Y) =
(^X: (static member ( &&^ ): _*_->_) x,y)
type True with
static member ( &&^ ) (True, True) = True
static member ( &&^ ) (True, False) = False
type False with
static member ( &&^ ) (False, True) = False
static member ( &&^ ) (False, False) = False
let b = True &&^ False
// val b: False = False
```
これがあれば,自然数同士の比較を実装した上で,静的アサーションができる.
```fsharp
// 静的アサーション用の関数.引数の型が True に評価されることを要求する.
let inline Assert (_: True) = ()
type S<'n> with
static member inline ( <^ ) (Z, S _) = True
static member inline ( <^ ) (S x, S y) = x <^ y
type Z with
static member inline ( <^ ) (_, Z) = False
Assert (S Z <^ S (S Z)) // OK
Assert (S (S Z) <^ S Z) // NGコンパイルエラー
```
ここでは `<` に相当する演算子 `<^` を実装したが,等価判定などのその他の比較演算子も同様に実装できる(ここでは割愛).これで,
1. ベクトル長や行列の次元は型レベルリテラルで管理する(加減乗除は型レベル計算で行う)
2. 境界外アクセスなどの検出は型レベル計算で静的アサーションを行ってチェックする
の2つを行うための道具が揃ったことになる[^actual-impl]
[^actual-impl]: なお,実際の FSharpPlus における[実装](https://github.com/fsprojects/FSharpPlus/blob/master/src/FSharpPlus/TypeLevel/TypeNat.fs)では,上で挙げたサンプルコードとは違う形で定義されている演算子が多いことをおことわりしておく.これは,これらの演算子を使ったインライン関数を定義した時に,「早すぎるオーバーロード解決」が起こって型変数に望まない形で制約がかかってしまうのを防ぐためのものである.具体的には,(実装が同じものを除いた)全ての種類の演算子をインライン関数でラップした[テストコード](https://github.com/fsprojects/FSharpPlus/blob/master/tests/FSharpPlus.Tests/TypeLevel.fs)のコンパイルがきちんと通るように定義してある
## 型レベル自然数から通常の自然数への変換,型レベル自然数のシングルトン値
なお,便利のために型レベル自然数から通常の自然数へ変換できるようにしておこう.
これも再帰的にオーバーロード解決を走らせるだけである.
```fsharp
// 型レベルの値を通常の値に変換するための関数
let inline RuntimeValue (x: ^X) = (^X: (static member RuntimeValue: ^X -> _) x)
type S<'n> with
static member inline RuntimeValue (S x: S< ^X >) = 1 + RuntimeValue x
type Z with
static member inline RuntimeValue Z = 0
let a = RuntimeValue (S (S (S Z)))
// val a: int = 3
```
`RuntimeValue` はインライン関数なので,実は `RuntimeValue (S (S (S Z)))` は 直接 `3` としてコンパイルされる.`1 + 1 + 1` のような処理が実行時に走ったりはしないので効率が良い.
また,型レベル自然数は型それ自身 `S<S<S<Z>>>` とその型を持つただ一つの値 `S (S (S Z))` の2つの形態がある
F# では関数呼び出しによるオーバーロード解決でしか新しい型を作れないので,型レベル自然数の値の方を主に取り扱うことになるが,一方で型パラメータとして渡された場合など,値の部分を伴わない形で型レベル自然数を渡されることがある.
そのときに,型から対応する値を生成できれば都合がいい.対応する値はただ一つなので,**型レベル自然数のシングルトン値** と呼ぶことにしよう.
任意の型レベル自然数からそのシングルトン値を得る関数 `Singleton` も再帰的にオーバーロード解決をかけて簡単に定義できる.
```fsharp
// 型レベル自然数の型のみからシングルトン値を得る関数
let inline Singleton< ^X when ^X: (static member Singleton: ^X -> ^X) > =
(^X: (static member Singleton: _ -> _) Unchecked.defaultof< ^X >)
type S<'n> with
static member inline Singleton (_: S< ^X >) =
S (Singleton< ^X >)
type Z with
static member inline Singleton (_: Z) = Z
let b = Singleton<S<S<S<Z>>>>
// val b: S<S<S<Z>>> = S (S (S Z))
```
例によって「戻り値の型によってオーバーロードが決まる」タイプの関数なので,[12日の記事](https://7colou.red/blog/2020/fsharp-advent.html)で説明したように,欲しい戻り値の型をダミー引数として渡させることでオーバーロードを解決している.
また `Singleton` もインライン展開されるので `Singleton<S<S<S<Z>>>>` は直接 `S (S (S Z))` としてコンパイルされる.
これらの道具を使って,固定長のベクトルと行列を実装していくことになる.ただし,ベクトルと行列でやることはそんなに変わらないので,固定長ベクトルの実装についてのみ説明をする.
## 固定長ベクトルを実装,しかし……
道具が揃ってしまえばやることはシンプルである.
* 配列をラップして,新しくベクトル型を作る.
* ベクトル型では,要素の型に加えて,ベクトルの長さを型レベル自然数で持つようにする.
* ベクトルに対する演算を行う時に,型レベル自然数を使って,ベクトルの長さに関する制約を静的アサーションする.
ベクトルの長さが絡むような演算はいろいろ考えられるが,簡単なものとして要素へのアクセスを例として説明する.
これらを素直に実装すれば以下のようになる.
```fsharp
type Vector<'t, 'length> = {
items: 't[] // 要素を格納する配列
length: 'length // 長さを表す型レベル自然数
}
module Vector =
// 型レベル自然数 index を指定してindex 番目の要素を取り出す関数
let inline get (index: ^index) (vec: Vector<'t, ^length>) : 't =
// index と length を比較して,配列の範囲内へのアクセスかを静的にチェックする
Assert (index <^ vec.length)
// 実際に index 番目の要素を取得して返す
vec.items.[RuntimeValue index]
```
これを使ってみよう.
```fsharp
let vec = { items = [| 1; 2; 3 |]; length = S (S (S Z)) }
let a = vec |> Vector.get (S (S Z))
// val a: int = 3
let b = vec |> Vector.get (S (S (S Z)))
// compilation error
```
う〜〜〜〜むやりたいことはできているのだが明らかな問題点が2つある
1. ベクトルの作成時に型レベル自然数を手動で入力させているようでは型安全ではない.
2. 要素数が定数の時に `S (S (S Z))` などと書くのが面倒くさい.
そこで以下の2つを行ってそれぞれの問題を解決したい
1. タプルを受け取って,そのタプルの arity を数えつつベクトルを作成する関数 `vector` を作る.
2. Type Provider を使って,`3` といったリテラルから型レベル自然数を作れるようにするまた型レベル自然数をそのまま受け取る関数に加えてType Provider を使ってリテラルを入力させるバージョンの関数を用意する.
1 については [以前の記事](https://cannorin.github.io/blog/2019/fsharp-advent.html) で詳しく解説しているのでそちらを参照していただきたい記事中の「例1: タプルの arity を数える」と「例2: 同じ型のみのタプルをリストに変換する」を使えばすぐ作れてしまうことは分かっていただけると思う.
実際,`vector``CountTuple``TupleToList` を用いて次のように実装されている.
```fsharp
let inline vector (definition: '``a * 'a * .. * 'a``) : Vector<'a, 'n> =
{ items = Array.ofList (TupleToList.Invoke definition);
length = CountTuple.Invoke definition }
```
本記事では 2 について説明する.
## Type Provider を用いて型レベル自然数の入力をシンプルにする
Type Provider 自体の説明やType Providers の作り方に関する一般的な説明は知っているものとして,ここではしない.
Type Provider を使って型レベル自然数を作ることはそこまで難しくない.生成する型レベル自然数に対して,適切な `FSharp.Quotations.Expr` を作ってあげてそれを使ってあげればよいFSharpPlus では以下のようにして `FSharp.Quotations.Expr` を構成している.
```fsharp
module internal ProviderUtils =
let private sTy = typedefof<S<Z>>.GetGenericTypeDefinition()
let private zTy = typeof<Z>
let mutable private memot = Map.ofList [0, zTy]
let rec createNatType n =
match memot |> Map.tryFind n with
| Some x -> x
| None ->
if n > 0 then
let x = sTy.MakeGenericType(createNatType(n-1))
memot <- memot |> Map.add n x
x
else
zTy
let mutable private memov = Map.ofList [0, <@@ Z @@>]
let rec createNatValue n =
match memov |> Map.tryFind n with
| Some x -> x
| None ->
if n > 0 then
let uci = createNatType n |> FSharpType.GetUnionCases
|> Seq.head
let x = Expr.NewUnionCase(uci, [createNatValue(n-1)])
memov <- memov |> Map.add n x
x
else
<@@ zTy @@>
```
この2つが出来てしまえば例えば `ProvidedMethod` などで雑に作ってあげてもよいのだが,しかし
```fsharp
let three = TypeNat.Create<3>()
```
などというのは最後の `()` がなんとも気持ち悪いし,型名を書かないといけないときは結局 `S<S<S<Z>>>` と書かないといけないのも嫌だ.
そこで,`TypeNat<3>` という型自体が `S<S<S<Z>>>` を継承するようにして[^inherit]`S<S<S<Z>>>` の代わりに `TypeNat<3>` と書けるようにした上で,
```fsharp
let three = TypeNat<3>.Value
```
として値も作れるようにする.
[^inherit]: レコードやヴァリアントは普通は継承してクラスを作ったりはできないのだがType Provider の `ProvidedType``baseType` にはできてしまう
これは `DefineStaticParameters` 内で返す `ty` を差し替えてしまえばよいだけなので,簡単に実装できる.
```fsharp
[<EditorBrowsable(EditorBrowsableState.Never); TypeProvider>]
type NatProvider (cfg) as this =
inherit TypeProviderForNamespaces(cfg)
let thisAsm = Assembly.GetExecutingAssembly()
let root = "FSharpPlus.TypeLevel"
let baseTy = typeof<obj>
let prm = [ProvidedStaticParameter("value", typeof<int>)]
let ty = ProvidedTypeDefinition(thisAsm, root, "TypeNat", Some baseTy)
do ty.DefineStaticParameters(
parameters = prm,
instantiationFunction = (fun tyName prmValues ->
match prmValues with
| [| :? int as value |] ->
if value < 0 then
failwith "value is negative"
let n = ProviderUtils.createNatValue value in
let ty = ProvidedTypeDefinition(thisAsm, root, tyName, baseType = Some n.Type)
let valuet = ProvidedProperty("Value", n.Type, isStatic = true, getterCode = fun _ -> Expr.Coerce(n, n.Type))
let sing = ProvidedMethod("Singleton", [ ProvidedParameter("defaultValue", ty)], ty, (fun _ -> Expr.Coerce(n, ty)), isStatic = true)
valuet.AddXmlDoc <| sprintf "The value-level representation of type-level natural '%i'." value
ty.AddMember valuet
ty.AddMember sing
ty.AddXmlDoc <| sprintf "Type-level natural '%i'." value
ty
| _ -> failwith "unexpected parameter values"
)
)
do this.AddNamespace(root, [ty])
```
問題なのが `vec |> Vector.Get<3>` のようなことをしたい場合だType Provider はインラインメソッドを provide できないので,`Vector.Get<3>(vec)` のようなメソッドを生やすことはできない対処法はいくつかあるが綺麗に書けるようなものは1つしか見つけられなかった
まず,次のような型を作っておく.
```fsharp
// この属性は IDE の補完候補に出てこないようにするためのもの
[<EditorBrowsable(EditorBrowsableState.Never)>]
module VectorProviderHelpers =
// 中身が空のクラス
type Get<'i>() = class end
```
その上で,次のような拡張メソッドを用意しておく.
```fsharp
[<Extension>] // この型に生えている静的メソッドを拡張メソッドとして扱う属性
[<EditorBrowsable(EditorBrowsableState.Never)>] // 同上
type VectorProviderHelpersImpl =
[<Extension>]
static member inline Invoke (_: unit -> Get< ^i >, vec) =
Vector.get Singleton< ^i > vec
```
最後に Type Provider では,例えば `Vector.Get<3>` に対して次のようなメソッドを生成するようにする.
```fsharp
static member Get<3>() = new Get<S<S<S<Z>>>>();
```
するとどうなるか.仮にユーザが `let x = Vector.Get<3>()` としたとすると,`x` には `Get<S<S<S<Z>>>>` 型の値が代入される.
しかし,最後の `()` を書かない場合,`Vector.Get<3>` という式は全体として `unit -> Get<S<S<S<Z>>>>` という型を持つ.
ここで,先ほど我々は型 `unit -> Get<S<S<S<Z>>>>` に対して `Invoke` という拡張メソッドを定義した[^ext-method]
`Invoke``Get<_>` に加えてベクトルを受け取り,`Get<_>` の中に入っている型レベル自然数のシングルトンを取得して,合わせて `Vector.get` を呼び出している.つまり,
```fsharp
let x1 = vec |> Vector.Get<3>.Invoke
let x2 = vec |> Vector.get (S (S (S Z)))
```
この2つは全く同じように働く前者が後者に変換されるのだこれで型レベル自然数を書くのが面倒な問題が解決した
[^ext-method]: 矢印型(関数型)にも拡張メソッドを生やせるのは私もビックリした
## まとめ・課題
今まで使ってきた SRTP と Type Provider はどちらもコンパイル時に処理されるので,この `Vector` の実行時オーバーヘッドとなりうる部分はラップされた配列 `items` を取り出す処理だけである.リフレクションや仮想呼び出しなどは一切使っていない.よって,配列の代わりに `Vector` を使うことによるオーバーヘッドはほぼ最小限に抑えられている.
それに加えて,`vector (1, 2, 3)``Vector.Get<2>.Invoke` のような簡潔で直観的な API を用意することが出来たF# ユーザにとって使いやすいものになっていると言えるだろう.
そしてこのようなものを言語拡張に頼らず F# 自体の言語機能だけで実装できてしまうことはF# の言語機能の隠された「強さ」を示している[^turing-complete]
[^turing-complete]: 以前F# 上で[型レベルラムダ計算](https://github.com/cannorin/FSharp.TypeLevel/)を実装した.おそらく F# の型システムは Turing 完全である
一方課題もあるn = 100 程度から型レベル自然数同士の演算が導入する制約の個数が F# コンパイラの内部の制限を超えてしまうため長さが100程度以上のベクトルを作ってもうまく使うことができないこれについては今後時間ができたら改善を試みてみたい
## はい
よかったですね
----
[今年の F# Advent Calendar](https://qiita.com/advent-calendar/2020/fsharp) はたくさんの記事が集まってすごかったですね.私も精進しなくては……
参加した皆様方,お疲れ様でした.

View File

@@ -0,0 +1,107 @@
---
title: 情報科学若手の会2022に参加した
description: ブログエントリを書くまでが若手の会
date: !!timestamp 2022-09-26T15:00:00.0000000+09:00
category: Programming
tags: ["Diary"]
---
第55回[情報科学若手の会](https://wakate.org/)に参加してました.
そもそも現実世界でイベントに参加するのがだいぶ久しぶりだったので不安でしたが,
さすがは50年以上も開催されているイベントなだけありすぐに周りと打ち解けられるようになっていてとても良かったです
また,以前にお世話になった方々と予想外のタイミングで再会することができ,
積もる話をたくさんすることができて嬉しかったです.
2泊3日って長いな途中でしんどくなったりしないかな人と話すのが苦手なのでと参加する前には思っていましたが
上記の通り実際にはとんだ杞憂でとても楽しく過ごせ終わってみればあっという間の3日間でした
参加していたみなさま,お疲れ様でした&幹事の皆様とスポンサーの方々,ありがとうございました.
「ブログエントリを書くまでが若手の会」とのことなので,ブログエントリをしたためました.
参加記を書いたことが今までなかったので,つたないエントリになってしまっていそうで,
こんな感じで本当にいいのか不安ですが,後から書く方々のハードルを下げるのに役立ちそう?なので公開します.
> エントリに他の方々の具体的な人名がまったく出てこないのは,人の名前を覚えるのがとても苦手で,
> 視覚情報で覚えているため,後から確認もできないためです.ごめんなさい……
## 宇宙猫を生成した
交流イベントとして「お題の画像が与えられ,[Stable Diffusion](https://github.com/CompVis/stable-diffusion) でなるべく近い画像を生成する」というトンチキな競技が開催されてぶったまげました.
> [宇宙猫](https://dic.nicovideo.jp/a/%E5%AE%87%E5%AE%99%E7%8C%AB)を出したいが "meme" というプロンプトが文字を誘導してしまい困っている図
![](https://imgur.com/Lunk1HS.png)
チームのメンバーの技術(とジャンケン運)に支えられて,準優勝を飾ることができました.
## 発表を聴いてきた
他の方の発表をたくさん聴いてきました.
どの発表も大変面白く,刺激になりました.特に,以下の内容の発表は全く知らない分野の話だったので,聞けて良かったです:
* 情報モラル教育の話
- 生まれた時から Twitter があるような世代にどうネットリテラシーを教育するか,は本当に難しい問題だと思います
* ユーザ体験と広告効果のどちらも損なわないようにする話
- 両者の立場にしっかり立ってバランスを取るのは大変だと思うので尊敬します
* 様々な会社の方々の,実際どういう仕事をしているのかの話
- 外に出せないような話も色々聞けてよかったです(なので曖昧な書き方)
自分の専門と全く異なる分野の方々の話を聞ける機会は,こういうイベントに行かないとなかなかないものです.
## 発表をしてきた
<iframe class="speakerdeck-iframe" style="border: 0px none; background: rgba(0, 0, 0, 0.1) padding-box; margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 560px; height: 314px;" src="https://speakerdeck.com/player/bd4469787fcd46a787ca7f21e93f4b4f?slide=1" title="AltJS を作るなら型変換を入れた方がいい" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true" data-ratio="1.78343949044586" frameborder="0"></iframe>
せっかくなので,関数型 AltJS で JS/TS のクラス・インターフェースと自然な interop をするのが結構難しい,という話をしてきました.
外部配信なし&巻き進行でマニアックなテーマ,ということでウケるかとても不安でしたが,結構反応をいただけて嬉しかったです.
また LT 枠では以前大学でやった「5分でわかるモナド」をやりましたこっちは完全にウケ狙いだったので滑らなくてよかったです
## ゲームで遊んだ
結構な量のアナログゲームが持ち込まれていて2日目のナイトセッションでそのうちの2つを楽しく遊びました
* [Figgle](https://figgie.com/)
- 今回のスポンサー企業の一つでもある [Jane Street](https://www.janestreet.com/) が開発
- 市場取引と交渉をテーマにしたゲーム
- 駆け引きの要素が非常に強く興奮した.[EVE Online](https://www.eveonline.com/) のマーケットPvPを思い出した
- お金を表すチップのさわり心地が大変良かった
* [チューリップ・バブル](https://gamemarket.jp/game/211)
- 17世紀に起こったチューリップ・バブルがモチーフ
- ランダムな価格変動(と,最後のバブル破裂)を読んで利益を最大化するゲーム
- 駆け引きの要素が Figgle より軽い代わりに長期的なEXIT戦略を要求されて面白かった
- ルールを理解するのに30分くらいかかった気がする
## 軽井沢観光をしてきた
3日目の解散後に少しだけ軽井沢観光に行きました
フォロワーにおすすめされた[美術館](https://www.senju-museum.jp/)に行き,ご飯を食べて帰宅しました.
> 偶然遭遇した[ゴママヨ](https://thinaticsystem.com/glossary/gomamayo)
![](https://imgur.com/MoncmAx.jpg)
## 来年もまた参加したい
とても楽しく,学びがあるイベントでした.来年も(時間などが許せば)ぜひ参加したいです.
ちなみにわたしは関西在住なので,次回は関西から到達しやすい静岡県がいいな~という話をしていましたが,
若手の会がよく利用していたらしい静岡の旅館の[倒産が判明](https://www.ryoko-net.co.jp/?p=107169)してしまいました.かなしい.
## みなさんも参加してみては
幹事の方々が強調されていたのが,「若手じゃない」「技術が足りない」といった尻込みしそうな要素をできる限り払拭したい,ということでした.
実際わたしは今回がはじめての参加で,しかも専門分野が抽象的な数学&高レイヤなプログラミングなので,
セッションのほとんどが馴染みの薄い内容でしたが,発表者の方々のわかりやすい講演とフレンドリーな
雰囲気のおかげで,置いていかれてしまう~といったことは全くありませんでした.
絶妙にゆるい雰囲気の,「学会」というよりは「合宿」のようで,しかしアカデミックな要素を外さない
イベントはなかなか貴重だと思います.少しでも情報科学に関わっている方は何かしら得られるものがあり,
そして何より楽しい会だと感じられるのではないでしょうか.
というわけで,来年度の告知も行われるであろう若手の会のホームページはこちらです: https://wakate.org/
来年の今頃にはコロナが完全収束していたらいいな…

View File

@@ -0,0 +1,383 @@
---
title: 「動的型つき」の言語を「型なし」と呼ぶのはおかしい,のか?
description: Twitter現・Xでのこの種の定期イベントはそろそろやめにしないか
date: !!timestamp 2023-08-19T16:15:00+09:00
category: Programming
tags: ["TypeScript", "Type System"]
---
ある日の Twitter 現・Xのタイムライン
> 動的型付けのことを型なしと呼ぶことに対しては様々な角度から殴ることが可能ですがどの角度から殴られたいですか?
> 定期的に出現する「動的型つきの言語を『型なしの言語』と呼ぶのはおかしい」論がなぜか軒並みかなり自信を持ったような書きぶりなのが不思議TaPLの1章だけでも読めばもっと整理された理解になると思うんですけど
> 型なしのことを動的型付けと呼ぶの、安全ではない言語が幅を利かせていた頃の名残だと思ってる(偏見)
> 動的型付けは静的には型の整合性を検証しないけど、型自体は言語機能としても概念としてもきちんと持ってるわけで、それを「自分が所望する安全性を提供しないから」って理由で「型など存在しない」まで言い切るのは流石に暴論というかただのディスりに片足突っ込んでね、と。
お前ら,いい加減にせい!!!!!!!!!! 何回目だよ[^1]
[^1]: 観測範囲では三回目.いい加減にしろ!
## 「型」とは何であるか
まず「型」という用語の定義についてハッキリさせておこう.おそらく全員に同意してもらえるであろう定義を提示する.
> 「型」とは,プログラム内で扱うデータを分類するものである
これに同意できないひとは,申し訳ないが回れ右してほしい.わたしには君を救うことはできない……
さて,実用的なプログラム言語であれば必ず `number``string` といった「プリミティヴ・データ型」なるものを持っているはずだ.
ここで JavaScript のような静的型検査をしないプログラム言語を考えてみると,変数 `foo` にデータが格納されているとして,`typeof foo` とかやると
`number` やら `string` やらが帰ってくるプログラム中で扱うデータが分類できているつまりJavaScript に型はある!というのが,
> 動的型付けは静的には型の整合性を検証しないけど、型自体は言語機能としても概念としてもきちんと持ってる
派の主張であろうそしてJavaScript を例にするが,静的型検査がないプログラム言語でも,型をチェックして安全に動くコードは書くことができる.
```javascript
// JavaScript
function greet(name) {
if (typeof name !== "string") throw new Error("name is not a string");
return `Hello, ${name}!`;
}
```
この状況を「型なし」と言ってしまうのは,確かにオカシイ気がする.
## 「型付け」とは何であるか
一方,両派がともに問題にしているのは「動的型付け」という用語の使い方だ.ここで,「動的型付け」とは「動的」に「型付け」をすることであろう.
「動的」は一般に「実行時」という意味と思ってよいだろう.逆に「静的」は「実行せずに」という意味である.残った「型付け」という用語について考えてみる.
「型」を「付け」ると言っているのだから,とりあえず *「型」なる何か**どこかに付ける* のであろう.ここで賢いあなたは「あれ?でもさっきの例だと,型って特に何もしなくても付いてたよ?」と思うかもしれない.その疑問は実は **この問題の核心を突いている** のだが,とりあえず置いておいてほしい.
「型付け」における「型」は一旦謎のままとして,ここでいう「型」なるものを付けたり付けなかったりできる言語として TypeScript を考えよう.例えば,以下の TypeScript コードには「型がついてない」ことに,皆さん同意できることであろう.
```typescript
// TypeScript
function greet(name) {
return `Hello, ${name}!`;
}
```
このコードに「型がついてない」ことに同意できない人も,わたしには救えないので,回れ右してほしい.型,ついてないですね? TypeScript コンパイラくんも `Parameter 'name' implicitly has an 'any' type.` とか言って怒っている.よろしい.では,型を付けてみよう.
```typescript
// TypeScript
function greet(name: string) {
return `Hello, ${name}!`;
}
```
型,付けれましたね? うむうむ,`: string` というものを *付けた* という確かな実感がある.これには TypeScript コンパイラくんも大満足.で,彼は何に怒っていたのだろう?彼は `Parameter 'name' implicitly has an 'any' type.` と言っていたTypeScript において「`any` 型である」というのは「どんな種類のデータでもある可能性がある」という意味だ.つまり,これは「`name` にはどんなデータでも入ってくる可能性があるよ」,言い換えれば「`name` に入るのがどんな種類のデータなのかわからないよ」と言っている.そして我々が `: string` と付けたことによって,「`name` には `string` 型のデータしか入ってこない」と知ったので,彼は怒らなくなったのである.まとめると,以下の通りである.
> 「型付け」とは,データが分類できてない状況下で,何らかの手段[^2]で,分類できている状態にすることである
[^2]: 型注釈を書くでも,型推論してもらうでも,なんでもよいので
以上の定義も,まぁ全員に同意してもらえることだと思う.
## 「動的型付け」という言い回しは,「型なし」と同じ理由でオカシイ!
ここで問題となってくるのが,「あれ?でもさっきの例だと,型って特に何もしなくても付いてたよ?」という疑問である.`string` とか `number` とかって,*TypeScript コンパイラくんが知らないだけ* で,何もしなくても最初から決まっていなかったっけ?
実際JavaScript の標準規格である [ECMAScript](https://262.ecma-international.org/14.0/) では,プログラム中で扱うデータは, `string` なり `number` なりに,最初から分類済である,ということになっている.
> Algorithms within this specification manipulate values each of which has an associated type. The possible value types are exactly those defined in this clause. Types are further subclassified into ECMAScript language types and specification types.
JavaScript で扱うデータは最初から分類済,ということは,先ほど書いた以下の JavaScript コードでは,**「分類できてないデータを改めて分類する」ような行為は一切していない** のだ.
```javascript
// JavaScript
function greet(name) {
if (typeof name !== "string") throw new Error("name is not a string");
return `Hello, ${name}!`;
}
```
ここでやっているのは,プログラムが預かり知らぬところで[^3] *最初から分類済である* ところの「型」を「チェック」しているのであって,「型付け」ではなく「型チェック」なのだ!
JavaScript プログラムは *最初からデータに型が付いている世界に生きている* のであって,これを「動的型付け」などと,あたかも型を *元々は付いてなくて,実行時になってから付けたかのように* 呼ぶのはおかしくないだろうか?
つまり,`string``number` などの型があるから「型なし」という言葉を使うのはオカシイ,と主張するならば, **型は最初からあるのだから「動的型付け」という言葉を使うのもオカシイ**,と主張しなければならないのだ!
[^3]: データの分類としての「型」にプログラムが干渉する余地はないということ.`typeof` を自分で実装したり,`typeof foo === "hogehoge"` になるような `foo` なり `hogehoge` を作ったりするのは不可能,と言ってもいい
## 「型付け」は,型をどこに付けているのか?
ところでTypeScript は JavaScript に静的な型システムを追加した言語である.つまり,全てのデータには最初から型が付いているし,`typeof` でそれをチェックすることもできる.実際,「`name` の型は `any` ですよ」と明示的に書くことで[^4],先ほどの JavaScript コードと同様のものが TypeScript でも書ける.
[^4]: 実は先ほどの例ではTypeScript コンパイラくんは「型注釈を(`: any` すらも)**明示的に書いていないこと**」に怒っている.嘘ついてごめん…… また,明示的に `any` を書く行為も ESLint で禁止できる
```typescript
// TypeScript
function greet2(name: any) {
if (typeof name !== "string") throw new Error("name is not a string");
return `Hello, ${name}!`;
}
```
一見 JavaScript と同じことをしているように見えるが,実は TypeScript はこの状況でも「型付け」を行う.
上のコードを [TypeScript Playground](https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAcwE4FN1QEwAowCGAtugFyIFgCeAlIgN4BQiiMwiuUVADunMPmLo6AQgC8YxACIAzlFQwwyKXSgALVHADuiMOh0BRVJtS4phEqxm64UCojkKlKgNzNEGKCFRIABgAl0ABsguAAaRAASegt0AF8RXzc4oA) で書いたので,そこで確認してほしいのだが,`typeof name``name` の上にマウスを置くと `(parameter) name: any` と出るが,``` return `Hello, ${name}!` ``` の `name` の上に置くと `(parameter) name: string` と出る.
つまり上のコードでは,`name` が持っているデータの型を `typeof` で実行時にチェックするコードを書くことで,`name` が `if` 文より下では確かに `string` 型を持つことをTypeScript コンパイラくんに教えてあげている(=型付けを手伝っている)のだ.
上の例からTypeScript コンパイラはデータそのものの型を気にしているというよりかは,それが入っている変数 `name` の型を気にしているのが分かると思う.データが手元にあればそのデータの型は分かって当たり前だが,ある変数にどのような型のデータが入ってくるかを *実行せず* に調べるのは,簡単にできることではなさそうだ.それを行うのが「型付け」だ. **「型付け」の対象はデータそのものではなく,定数や変数のような,データに直接対応するプログラムの構文である.** 「データに直接対応する構文」って定数や変数以外何があんねんと思う方は,
```typescript
// TypeScript
function greet(name: string) {
return `Hello, ${name}!`;
}
console.log(greet(greet("World"))); // Hello, Hello, World!
```
というコードを考えてほしい.ここで,`"World"` という定数が `string` 型を持っているから `greet` の引数として渡せるのと同様に,`greet("World")` という **式** も `string` 型を持っている[^5]ので,再び `greet` の引数として渡すことができるのだ.
[^5]: これは,`greet` が文字列を `return` することから TypeScript コンパイラが推論してくれている
この意味でも,**JavaScript は TypeScript 的な「型付け」なる行為を断じて行っていない** ことが分かると思うJavaScript が扱っている「型」はあくまでデータが持つ属性であって,データが入っている変数や,データを生成する式などは,型の概念と一切関係がない.変数にどういう型のデータが入ってくるか,なんて知ったことではないのだ.
また,上記の `typeof` を使った TypeScript コードのように,「データの型を *動的* にチェックすることで,構文に型付けをする」ことを「 *動的* 型付け」と呼んでいる,そして JavaScript をそのまま TypeScript として読んだときのように.「データの型の *動的* チェックしか,構文への型付けの手段がない」言語を「 *動的* 型つき言語」と呼んでいる人々もいるかもしれないが,この場合においても,実際の型付けは TypeScript コンパイラが *静的に* 行っているわけで,**紛らわしいからやめたほうがいい**,とわたしは思う.
> 「動的型付けされる」といった言い回しは誤っているといって差し支えなく、おそらく「動的検査される」と言い換えるべきである
って TaPL も言っている[^6]ことだし.
[^6]: [オーム社ウェブサイトの書籍ページ](https://www.ohmsha.co.jp/book/9784274069116/) からダウンロードできるサンプルで確認できる一文だ
## おまえはポジショントークをしているぞ
まとめよう.
* JavaScript プログラムくんの立場では,
- データは最初から分類済である.つまり **全ては最初から「型が付いている=型付き」** なのであって,断じて「型なし」ではなく,実行時に(=動的に)改めて "型を付ける" などということもしない.
- 変数はただのデータの入れ物であって,型とかは関係がない.
- データに型を付けるのは JavaScript 処理系の内部実装が勝手にやってることであってJavaScript プログラム的には知ったことではない.
- `typeof` を使うと,データに(最初から)付いている型をプログラム内でチェックすることができる(=「型チェック」).
* TypeScript コンパイラくんの立場では,
- 型注釈を書いたり,型推論したりすることで初めて,変数や式がどういう種類のデータを持つのか分かる. **その状態が「型が分かっている=型付き」** なのであって,注釈が書いてない・推論もできないのであれば「型が分からない=型なし」である.
- そもそも型をつける対象は変数や式である.
- 変数や式に型を付けるというのはTypeScript コンパイラくんが自分でやらなければならない行為である.
- `typeof` を使うと,「この範囲内ではこの型である」ことを TypeScript コンパイラに教えることができるその正しさは実行時にJavaScript 的な意味での)型チェックを行うことで保証される(=「動的な型付け」と言えなくもない??).
そしてTwitter現・Xで頻繁に見かけるこの種の議論に対するわたしの意見は以下だ
* 「『動的型つき』の言語を『型なし』と呼ばない」派の人はJavaScript プログラムくんの立場だ.
- 「型なし」という呼称に反対してもよい.
- しかし,その立場に立つのならば,**そもそも「動的型付き」と呼ぶべきではない**
* 「『動的型つき』の言語を『型なし』と呼ぶ」派の人はTypeScript コンパイラくんの立場だ.
- `any` 型が付いていても何もわからん,という意味で「型なし」と呼んでもいい.
- 「動的型付き」って呼び方はおかしい!「型なし」のほうが適切だ!という意味で言っているなら,それでよい.
- 「動的型付き」って「型なし」とも言えるよね,という意味で言っているなら,**前提がなんだかおかしい.**
* **一番中立なのは,「言語を『型なし』って呼ぶかは立場によるし,『動的型付き』って何?」派だ.**
- 上の二つは両者ともに中立ではなく,それぞれの立場で **ポジショントークをしている** ことに気をつけろ!
- 「動的型付け」という用語は,立場によらず**使うべきではない!**
でも,それだと「静的型検査をしていないプログラム言語」を指す **簡便で適切な用語が存在しない** ことになってしんどいですよね?
「動的型付け」と呼ぶと少なくとも「型なし」という用語を偉そうに否定できる立場ではなくなることを考えると,何か別な呼び方を考えたほうが良いんじゃないでしょうか[^7]
[^7]: 「動的型検査言語」はどうだろうか
それが無理なら,**中立の立場に立ったうえで,用語として適切でないのを許容して,伝わりやすさ重視で「動的型付け」と呼ぶ**,というのが現実的ではないでしょうか.結局みんなそう呼んでるし.みんな中立的になってね[^8]
[^8]: なお,静的型付けにも Church-style と Curry-style の二つの派閥が存在するが, Church-style 主義者に「型って何?」って聞くと「『型』とは『項』ッ!!」と返ってきて話がどんどんややこしくなるので,今回の記事では割愛する
> 「静的」という語を明示的に付加することがある。例えば、「静的型付きプログラミング言語」という言い方をする。これは、本書で考察しているような種類のコンパイル時解析と、Scheme のような言語で使われる動的型付け(あるいは潜在的型付け)とを区別するためである。後者ではヒープ中の異なる種類の構造を区別するのに実行時の型タグが利用される。「動的型付けされる」といった言い回しは誤っているといって差し支えなく、おそらく「動的検査される」と言い換えるべきであるが、標準的に使われる用語法である。
>
> ――― Benjamin C. Pierce 著,住井英二郎 監訳 『型システム入門』 一章より
## おまけ: 「静的型付け」は実は「データの分類」以上の行為である
みなさん,中立的になりましたね? では,以後は JavaScript 的な型付け(してないけど)のことを「動的型付け」,逆に TypeScript 的な型付けのことを「静的型付け」と呼ぶことにする.
ところでTypeScript を書いたことがあるかたはご存じかと思うがTypeScript ではクラスに対してクラス型というものがつく.
```typescript
// TypeScript
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x; this.y = y;
}
}
```
このように `Point` というクラスを定義すると,`Point` という名前の型が作られ,`Point` クラスのインスタンスは `Point` 型を持つことになる.
```typescript
// TypeScript
function distance(p1: Point, p2: Point): number {
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
}
console.log(distance(new Point(1,1), new Point(2, 0)));
```
このように,`Point` クラスのインスタンスを2つ受け取って点間の距離を返す関数 `distance` は,`Point` 型の引数を2つ取って点間の距離を `number` で返す,という型を持つことになる.「変数 `p1, p2` が `Point` クラスのインスタンスである」ことと,「変数 `p1, p2` が `Point` 型を持つ」ということを同一視している,ということがわかるだろうか.
また,何か別のクラス,例えば `Person` があったとして,`Person` クラスのインスタンスを `distance` に渡そうとしても TypeScript コンパイラ君が許さない.
```typescript
// TypeScript
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
console.log(distance(new Point(0, 0), new Person("Alice")));
// error: Argument of type 'Person' is not assignable to parameter of type 'Point'.
```
このように,**TypeScript の静的型付けでは,違うクラスのインスタンス(を表す構文)は違う型を持つ** とみなしていることがわかる.
ところがJavaScript の標準規格である [ECMAScript](https://262.ecma-international.org/14.0/) では,データは以下の種類の型しか持たないことになっている.
- `undefined`
- `null`
- ただし,`typeof` すると `"object"` を返す.
- `boolean`
- `string`
- `symbol`
- `number`
- `bigint`
- **`object`**
- 関数は特殊なオブジェクト.ただし,`typeof` は `"function"` を返す.
- コンストラクタも特殊なオブジェクト.ただし,`typeof` は `"function"` を返す.
実際JavaScript において `Point` クラスと `Person` クラスのインスタンスをそれぞれ作って,`typeof` をしてみると,両方とも `object` を返す.
```javascript
// JavaScript
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
console.log(typeof (new Point(0, 0))); // "object"
class Person {
constructor(name) {
this.name = name;
}
}
console.log(typeof (new Person("Alice"))); // "object"
```
つまり,**JavaScript の動的型付けでは,クラスのインスタンスは(違うクラスであっても)全て `object` 型を持つ** ことがわかる.では JavaScript ではこの種の区別をどうやっているのかというと,それぞれのオブジェクトが「どのクラスのコンストラクタから作られたのか」を表す **プロトタイプ(チェーン)** というものを持っている.`instanceof` という演算子を使うことで,オブジェクトが実際にあるクラスのインスタンスなのかどうかを,オブジェクトのプロトタイプを見てチェックすることができる.
```javascript
// JavaScript
function distance(p1, p2) {
if (!(p1 instanceof Point)) throw new Error("p1 is not a point.");
if (!(p2 instanceof Point)) throw new Error("p2 is not a point.");
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
}
```
ここで `instanceof` の右辺に現れている `Point` はいわゆる「型」ではなく,`Point` クラスのコンストラクタつまりこちらもオブジェクトであることに注意しようつまりJavaScript における `instanceof` は **二つのオブジェクトの関係を調べる** もので,それぞれのデータに最初から付いている型をチェックする動的型付けとは厳密には違う仕組みである.ちなみに,オブジェクトのプロトタイプはコンストラクタでインスタンスを作った瞬間に,つまり *実行時に設定される* ので,「動的に型っぽいものを付けている」という点で,むしろ **こちらのほうこそ「動的型付け」と呼べなくもない** のにも注意しよう.
以上のことからTypeScript の静的型付けは JavaScript の動的型付けよりも微妙に広い範囲を扱っていることがわかる.
一般に**静的型付けは,プログラム内で現れる不変条件全般を扱うことができる**.不変条件とは「いつもそうである条件」という意味だ.
TypeScript を例に取ると,
* 「変数 `foo` が `string` 型を持つ」ことは,「`foo` に入るデータの種類は `string` である」という条件を保証する
- つまり,`typeof foo` は *いつでも* `"string"` を返す
* 「変数 `bar` が `Point` 型を持つ」ことは,「`bar` に入るオブジェクトは `Point` クラスのインスタンスである」という条件を保証する
- つまり,`bar instanceof Point` は *いつでも* `true` を返す
しかし,クラス型が保証する「インスタンスである」という条件も,結局「データの持つ属性」と言えなくもないのでは?と思う諸氏もいることだろう.では,ある変数 `baz` が持つ型が,`baz` 自身に入るデータの属性とは *もはや何の関係もない条件を保証する* 例を挙げれば,「不変条件全般を扱える」ということに納得していただけるだろうか?
ここでTypeScript で `typeof` を使う例を思い出してほしい.
```typescript
// TypeScript
function greet2(name: any) {
if (typeof name !== "string") throw new Error("name is not a string");
return `Hello, ${name}!`;
}
```
この例において,``` `Hello, ${name}!` ``` の `name` の上にマウスカーソルを置くと `(parameter) name: string` と出てくるのは一体何故なのだろうか? 実は,`if (typeof name !== "string") throw ...` という文が,`if` 以下において `name` の型が `string` であることを保証しているのだ.これは,一旦別の関数に分割するとわかりやすい.
```typescript
// TypeScript
function isString(name: any): name is string {
return typeof name === "string";
}
function greet2(name: any) {
const nameIsString = isString(name);
if (nameIsString) return `Hello, ${name}!`;
else throw new Error("name is not a string");
}
```
上の例も [TypeScript Playground](https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABDAzgZSgJxmA5gCgA8AuRAQzAE8BKUw5FRFLHXRAbwChFFMBTKCExIolAA584wRPQC88xACJm2PIoDcnAL6dOoSLASJc-AQCZ8YMgFs+pCjQ7dEEBM0RXbASXQs8iWQYMVQJPPmpNZxhpSxs+H2DWal4BISQAAwAJPgAbHLgAGkQAEnYwrQBCdM0eXJQ+RCgAC0w4AHcPPg6AUUxWzHxFMIYPOChyJj9cRQjtIA) を用意してある.ここでも,``` `Hello, ${name}!` ``` の `name` の上にマウスカーソルを置くと `(parameter) name: string` と出てくるのが確認できるだろう.何故こんなことができるのか? それは,`isString` 関数の戻り値の型が `name is string` という珍妙な型を持っているからである.この型は文字通り,「`name` という変数が `string` 型を持っているかどうか」を表すもので,取りうる値は `true` もしくは `false` である.つまり,上の例では
```typescript
const nameIsString = isString(name);
```
と結果を変数 `nameIsString` に代入しているが,ここで `isString` 関数の戻り値の型が `name is string` であることは,**「もし `nameIsString` が `true` ならば,`name` の型は `string` である」** ことを保証しているのだ! これが保証されているからこそ,`if (nameIsString)` の中にある(= `nameIsString` が `true` であるケースの) ``` `Hello, ${name}!` ``` においては,`name` の型は `string` になっているのである.そして,`name is string` 型が保証している条件は `name` の型についてであって,`nameIsString` のデータの種類などは *もはやどうでもよくなっている* ことに注意してほしい.
以上の内容をまとめよう.
* **静的型付けは,プログラム内で現れる不変条件全般を扱うことができる**
- 「データの種類」(= `typeof` の結果)は,あくまで静的型付けで扱える不変条件の一つに過ぎない
- クラス型は,「特定の `class` のインスタンスである(≒ `instanceof` が `true` になる)」という不変条件である
- JavaScript 的には全て同じ `object` 型のデータであり,`typeof` では区別できない
- JavaScript におけるプロトタイプは,データの持つ「型」とは厳密には違う仕組みである
- `x is string` 型が保証する不変条件は,もはや自分自身のデータの種類とは関係ない
- `x is string` 型を返す関数が実際に返すデータは `true` か `false` である
* JavaScript におけるプロトタイプは,実行時にタグ付けされるという点では,動的に型付けしていると言えなくもない
- ただし仕様上では,厳密には「型」とは違う仕組みである
ちなみに,わたしが文中で
> > 「型」とは,プログラム内で扱うデータを分類するものである
>
> これに同意できないひとは,申し訳ないが回れ右してほしい.わたしには君を救うことはできない……
や,
> ```typescript
> // TypeScript
> function greet(name) {
> return `Hello, ${name}!`;
> }
> ```
>
> このコードに「型がついてない」ことに同意できない人も,わたしには救えないので,回れ右してほしい.
と言ったのは,このおまけで解説した内容を理解している人には釈迦に説法であって,
なおかつ「データの種類」としての「型」の用法を頑なに認めないような人は,
ガチガチの静的型付け原理主義者で救いようがない,ということです……
## おまけのおまけ: 「型推論」と「動的型付け」の混同について
以上の話がきちんと分かっていればTypeScript で
```typescript
// TypeScript
const x = "foo";
```
と書くと `x` の型が `string` になるといった,いわゆる「型推論」と,
JavaScript で
```javascript
// JavaScript
const x = "foo";
```
と型を指定せずに書ける,いわゆる「動的型付け」(という呼び方はオカシイと散々言ってるが)を,混同する **わけがない** のであるTypeScript コンパイラくんは型推論を頑張ったので,変数 `x` の型が `string` であることをキチンと知っているし,それこそが彼にとって大事なことだ.一方 JavaScript プログラムくん的には, `"foo"` というデータは生まれた時から `string` なのであって,それが入ってる変数 `x` の型なんてものは知ったことではないのである.

View File

@@ -0,0 +1,12 @@
export type Metadata = {
title: string;
description: string;
date: string;
category: string;
tags?: string[] | undefined;
};
export const data: Record<string, { metadata: Metadata }> = import.meta.glob(
"/src/routes/*/blog/*/**/*.md",
{ eager: true },
);

View File

@@ -0,0 +1,16 @@
import { type Metadata, data } from "./(articles)/data";
export async function load() {
const posts: (Metadata & { slug: string })[] = [];
for (const path in data) {
const post = data[path];
const slug = path
.replace("/src/routes/(main)/blog/(articles)/", "")
.replace("/+page.md", "");
posts.push({
...post.metadata,
slug,
});
}
return { posts };
}

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import "prismjs/themes/prism.min.css";
import { limitWidth } from "$lib/constants";
import { cn } from "$lib/utils";
let { data } = $props();
</script>
<main class={cn(limitWidth, "flex flex-col items-center gap-12 lg:gap-16")}>
<div class="flex flex-col gap-6 w-full max-w-[720px]">
{#each data.posts as post}
{@const date = new Date(post.date)}
<article>
<h1 class="text-lg">
<a href="/blog/{post.slug}" style="view-transition-name: article-{post.slug}-title">
{post.title}
</a>
</h1>
<div class="text-xs flex flex-wrap-reverse justify-between" style="view-transition-name: article-{post.slug}-meta">
<ul class="flex gap-3">
<li class="text-primary font-bold">#{post.category}</li>
{#each post.tags ?? [] as tag}
<li>#{tag}</li>
{/each}
</ul>
<time datetime={date.toISOString()}>{date.getFullYear()}.{date.getMonth() + 1}.{date.getDate()}</time>
</div>
</article>
{/each}
</div>
</main>

View File

@@ -6,11 +6,7 @@ import Card from "./card.svelte";
</script>
<!-- Sections -->
<main class={cn(limitWidth, "flex grow flex-col items-center gap-12 lg:gap-16 py-8")}>
<h1 class="font-display text-4xl md:text-5xl lg:text-6xl">
<a href="/">cannorin.net</a>
</h1>
<main class={cn(limitWidth, "flex grow flex-col items-center gap-12 lg:gap-16")}>
<section class="flex flex-col items-center md:flex-row md:items-start gap-4 lg:gap-8">
<h2 class="sr-only">自己紹介</h2>

View File

@@ -6,11 +6,7 @@ import Card from "./card.svelte";
</script>
<!-- Sections -->
<main class={cn(limitWidth, "flex grow flex-col items-center gap-12 lg:gap-16 py-8")}>
<h1 class="font-display text-4xl md:text-5xl lg:text-6xl">
<a href="/">cannorin.net</a>
</h1>
<main class={cn(limitWidth, "flex grow flex-col items-center gap-12 lg:gap-16")}>
<section class="flex flex-col items-center md:flex-row md:items-start gap-4 lg:gap-8">
<h2 class="sr-only">自己紹介</h2>

View File

@@ -1,6 +1,9 @@
import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { mdsvex } from "mdsvex";
import rehypeKatex from "rehype-katex-svelte";
import remarkFootnotes from "remark-footnotes";
import remarkMath from "remark-math";
/** @type {import('@sveltejs/kit').Config} */
const config = {
@@ -11,6 +14,8 @@ const config = {
preprocess: [
mdsvex({
extensions: [".md"],
remarkPlugins: [remarkMath, remarkFootnotes],
rehypePlugins: [rehypeKatex],
}),
vitePreprocess(),
],

View File

@@ -82,15 +82,21 @@ export default {
"--tw-prose-bold": "rgb(var(--foreground))",
"--tw-prose-counters": "rgb(var(--primary))",
"--tw-prose-bullets": "rgb(var(--primary))",
"--tw-prose-hr": "rgb(var(--muted-foreground))",
"--tw-prose-quotes": "rgb(var(--muted-foreground))",
"--tw-prose-quote-borders": "rgb(var(--muted-foreground))",
"--tw-prose-hr": "rgb(var(--muted))",
"--tw-prose-quotes": "rgb(var(--foreground) / 0.75)",
"--tw-prose-quote-borders": "rgb(var(--foreground) / 0.75)",
"--tw-prose-captions": "rgb(var(--muted))",
"--tw-prose-code": "rgb(var(--foreground))",
"--tw-prose-pre-code": "rgb(var(--muted))",
"--tw-prose-pre-bg": "rgb(var(--muted))",
"--tw-prose-th-borders": "transparent",
"--tw-prose-td-borders": "rgb(var(--primary))",
"code::before": {
content: '""',
},
"code::after": {
content: '""',
},
code: {
"font-weight": 400,
},
},
},
light: {

473
yarn.lock
View File

@@ -904,7 +904,46 @@ __metadata:
languageName: node
linkType: hard
"@types/unist@npm:^2.0.0, @types/unist@npm:^2.0.2, @types/unist@npm:^2.0.3":
"@types/hast@npm:^2.0.0":
version: 2.3.10
resolution: "@types/hast@npm:2.3.10"
dependencies:
"@types/unist": "npm:^2"
checksum: 10c0/16daac35d032e656defe1f103f9c09c341a6dc553c7ec17b388274076fa26e904a71ea5ea41fd368a6d5f1e9e53be275c80af7942b9c466d8511d261c9529c7e
languageName: node
linkType: hard
"@types/hast@npm:^3.0.0":
version: 3.0.4
resolution: "@types/hast@npm:3.0.4"
dependencies:
"@types/unist": "npm:*"
checksum: 10c0/3249781a511b38f1d330fd1e3344eed3c4e7ea8eff82e835d35da78e637480d36fad37a78be5a7aed8465d237ad0446abc1150859d0fde395354ea634decf9f7
languageName: node
linkType: hard
"@types/katex@npm:^0.16.0":
version: 0.16.7
resolution: "@types/katex@npm:0.16.7"
checksum: 10c0/68dcb9f68a90513ec78ca0196a142e15c2a2c270b1520d752bafd47a99207115085a64087b50140359017d7e9c870b3c68e7e4d36668c9e348a9ef0c48919b5a
languageName: node
linkType: hard
"@types/prismjs@npm:1.26.5":
version: 1.26.5
resolution: "@types/prismjs@npm:1.26.5"
checksum: 10c0/5619cb449e0d8df098c8759d6f47bf8fdd510abf5dbdfa999e55c6a2545efbd1e209cc85a33d8d9f4ff2898089a1a6d9a70737c9baffaae635c46852c40d384a
languageName: node
linkType: hard
"@types/unist@npm:*, @types/unist@npm:^3.0.0":
version: 3.0.3
resolution: "@types/unist@npm:3.0.3"
checksum: 10c0/2b1e4adcab78388e088fcc3c0ae8700f76619dbcb4741d7d201f87e2cb346bfc29a89003cfea2d76c996e1061452e14fcd737e8b25aacf949c1f2d6b2bc3dd60
languageName: node
linkType: hard
"@types/unist@npm:^2, @types/unist@npm:^2.0.0, @types/unist@npm:^2.0.2, @types/unist@npm:^2.0.3":
version: 2.0.11
resolution: "@types/unist@npm:2.0.11"
checksum: 10c0/24dcdf25a168f453bb70298145eb043cfdbb82472db0bc0b56d6d51cd2e484b9ed8271d4ac93000a80da568f2402e9339723db262d0869e2bf13bc58e081768d
@@ -1036,6 +1075,13 @@ __metadata:
languageName: node
linkType: hard
"bcp-47-match@npm:^2.0.0":
version: 2.0.3
resolution: "bcp-47-match@npm:2.0.3"
checksum: 10c0/ae5c202854df8a9ad4777dc3b49562578495a69164869f365a88c1a089837a9fbbce4c0c44f6f1a5e44c7841f47e91fe6fea00306ca49ce5ec95a7eb71f839c4
languageName: node
linkType: hard
"binary-extensions@npm:^2.0.0":
version: 2.3.0
resolution: "binary-extensions@npm:2.3.0"
@@ -1194,6 +1240,13 @@ __metadata:
languageName: node
linkType: hard
"comma-separated-tokens@npm:^2.0.0":
version: 2.0.3
resolution: "comma-separated-tokens@npm:2.0.3"
checksum: 10c0/91f90f1aae320f1755d6957ef0b864fe4f54737f3313bd95e0802686ee2ca38bff1dd381964d00ae5db42912dd1f4ae5c2709644e82706ffc6f6842a813cdd67
languageName: node
linkType: hard
"commander@npm:^4.0.0":
version: 4.1.1
resolution: "commander@npm:4.1.1"
@@ -1208,6 +1261,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^8.3.0":
version: 8.3.0
resolution: "commander@npm:8.3.0"
checksum: 10c0/8b043bb8322ea1c39664a1598a95e0495bfe4ca2fad0d84a92d7d1d8d213e2a155b441d2470c8e08de7c4a28cf2bc6e169211c49e1b21d9f7edc6ae4d9356060
languageName: node
linkType: hard
"cookie@npm:^0.6.0":
version: 0.6.0
resolution: "cookie@npm:0.6.0"
@@ -1239,6 +1299,13 @@ __metadata:
languageName: node
linkType: hard
"css-selector-parser@npm:^1.0.0":
version: 1.4.1
resolution: "css-selector-parser@npm:1.4.1"
checksum: 10c0/4a89a7b61072cf0e4d09e8abbb9a77bc661232b6fe6a6fe51ba775757bae0e3fc462b0db4c9a857da55afb89a1c1746a7b2ec1200f639c539556ebdc758b0101
languageName: node
linkType: hard
"css-tree@npm:^2.3.1":
version: 2.3.1
resolution: "css-tree@npm:2.3.1"
@@ -1303,6 +1370,13 @@ __metadata:
languageName: node
linkType: hard
"dequal@npm:^2.0.0":
version: 2.0.3
resolution: "dequal@npm:2.0.3"
checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888
languageName: node
linkType: hard
"detect-libc@npm:^2.0.3":
version: 2.0.3
resolution: "detect-libc@npm:2.0.3"
@@ -1317,6 +1391,15 @@ __metadata:
languageName: node
linkType: hard
"devlop@npm:^1.0.0, devlop@npm:^1.1.0":
version: 1.1.0
resolution: "devlop@npm:1.1.0"
dependencies:
dequal: "npm:^2.0.0"
checksum: 10c0/e0928ab8f94c59417a2b8389c45c55ce0a02d9ac7fd74ef62d01ba48060129e1d594501b77de01f3eeafc7cb00773819b0df74d96251cf20b31c5b3071f45c0e
languageName: node
linkType: hard
"didyoumean@npm:^1.2.2":
version: 1.2.2
resolution: "didyoumean@npm:1.2.2"
@@ -1324,6 +1407,15 @@ __metadata:
languageName: node
linkType: hard
"direction@npm:^2.0.0":
version: 2.0.1
resolution: "direction@npm:2.0.1"
bin:
direction: cli.js
checksum: 10c0/dce809431cad978e0778769a3818ea797ebe0bd542c85032ad9ad98971e2021a146be62feb259d7ffe4b76739e07b23e861b29c3f184ac8d38cc6ba956d5c586
languageName: node
linkType: hard
"dlv@npm:^1.1.3":
version: 1.1.3
resolution: "dlv@npm:1.1.3"
@@ -1406,7 +1498,7 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^4.2.0":
"entities@npm:^4.2.0, entities@npm:^4.5.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
@@ -1710,6 +1802,157 @@ __metadata:
languageName: node
linkType: hard
"hast-util-from-dom@npm:^5.0.0":
version: 5.0.1
resolution: "hast-util-from-dom@npm:5.0.1"
dependencies:
"@types/hast": "npm:^3.0.0"
hastscript: "npm:^9.0.0"
web-namespaces: "npm:^2.0.0"
checksum: 10c0/9a90381e048107a093a3da758bb17b67aaf5322e222f02497f841c4990abf94aa177d38d5b9bf61ad07b3601d0409f34f5b556d89578cc189230c6b994d2af77
languageName: node
linkType: hard
"hast-util-from-html-isomorphic@npm:^2.0.0":
version: 2.0.0
resolution: "hast-util-from-html-isomorphic@npm:2.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
hast-util-from-dom: "npm:^5.0.0"
hast-util-from-html: "npm:^2.0.0"
unist-util-remove-position: "npm:^5.0.0"
checksum: 10c0/fc68d9245e794483a802d5c85a9f6c25959e00db78cc796411efc965134f3206f9cc9fa38134572ea781ad74663e801f1f83202007b208e27a770855566a62b6
languageName: node
linkType: hard
"hast-util-from-html@npm:^2.0.0":
version: 2.0.3
resolution: "hast-util-from-html@npm:2.0.3"
dependencies:
"@types/hast": "npm:^3.0.0"
devlop: "npm:^1.1.0"
hast-util-from-parse5: "npm:^8.0.0"
parse5: "npm:^7.0.0"
vfile: "npm:^6.0.0"
vfile-message: "npm:^4.0.0"
checksum: 10c0/993ef707c1a12474c8d4094fc9706a72826c660a7e308ea54c50ad893353d32e139b7cbc67510c2e82feac572b320e3b05aeb13d0f9c6302d61261f337b46764
languageName: node
linkType: hard
"hast-util-from-parse5@npm:^8.0.0":
version: 8.0.2
resolution: "hast-util-from-parse5@npm:8.0.2"
dependencies:
"@types/hast": "npm:^3.0.0"
"@types/unist": "npm:^3.0.0"
devlop: "npm:^1.0.0"
hastscript: "npm:^9.0.0"
property-information: "npm:^6.0.0"
vfile: "npm:^6.0.0"
vfile-location: "npm:^5.0.0"
web-namespaces: "npm:^2.0.0"
checksum: 10c0/921f40d7bd71fe7415b68df5e2d53ba62f0a35808be0504fa24584e6f6a85bfbf14dc20d171c7ccc1cf84058bcc445d12a746598d324cece1ec1e52ea9d489af
languageName: node
linkType: hard
"hast-util-from-string@npm:^2.0.0":
version: 2.0.0
resolution: "hast-util-from-string@npm:2.0.0"
dependencies:
"@types/hast": "npm:^2.0.0"
checksum: 10c0/8d0e5f5aac7ffee0adc08327913122642af552ca52dbac424769f1941695f9b4fd45d41be538faf078089e2e0abb56fdfe09009427120b5e0ae04bd4cb93a53d
languageName: node
linkType: hard
"hast-util-has-property@npm:^2.0.0":
version: 2.0.1
resolution: "hast-util-has-property@npm:2.0.1"
checksum: 10c0/0e3956ae8ad40148d908be94717c04bd237cabaf5a72093e1992a17cc9f143fd1524b27ae9af229893797babbaaa666f8735e83636523859c935a3d6b230c2ff
languageName: node
linkType: hard
"hast-util-is-element@npm:^3.0.0":
version: 3.0.0
resolution: "hast-util-is-element@npm:3.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
checksum: 10c0/f5361e4c9859c587ca8eb0d8343492f3077ccaa0f58a44cd09f35d5038f94d65152288dcd0c19336ef2c9491ec4d4e45fde2176b05293437021570aa0bc3613b
languageName: node
linkType: hard
"hast-util-parse-selector@npm:^4.0.0":
version: 4.0.0
resolution: "hast-util-parse-selector@npm:4.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
checksum: 10c0/5e98168cb44470dc274aabf1a28317e4feb09b1eaf7a48bbaa8c1de1b43a89cd195cb1284e535698e658e3ec26ad91bc5e52c9563c36feb75abbc68aaf68fb9f
languageName: node
linkType: hard
"hast-util-select@npm:^5.0.5":
version: 5.0.5
resolution: "hast-util-select@npm:5.0.5"
dependencies:
"@types/hast": "npm:^2.0.0"
"@types/unist": "npm:^2.0.0"
bcp-47-match: "npm:^2.0.0"
comma-separated-tokens: "npm:^2.0.0"
css-selector-parser: "npm:^1.0.0"
direction: "npm:^2.0.0"
hast-util-has-property: "npm:^2.0.0"
hast-util-to-string: "npm:^2.0.0"
hast-util-whitespace: "npm:^2.0.0"
not: "npm:^0.1.0"
nth-check: "npm:^2.0.0"
property-information: "npm:^6.0.0"
space-separated-tokens: "npm:^2.0.0"
unist-util-visit: "npm:^4.0.0"
zwitch: "npm:^2.0.0"
checksum: 10c0/af9b27bacfbb1c0423e64d9ef4a717c7a5f36de3f35b0173b0b13cec514c4eff2b62c8c5c7fb155eb81e65f35206860fb9965d49474a17daa341fd5ef09ec6c5
languageName: node
linkType: hard
"hast-util-to-string@npm:^2.0.0":
version: 2.0.0
resolution: "hast-util-to-string@npm:2.0.0"
dependencies:
"@types/hast": "npm:^2.0.0"
checksum: 10c0/9cf78d0de776e379476408d8363eeb690421e1b042919cfd97651eb968dbfe4ca9f5e4e23478a8d2c0d84a5432478d02d4c8ceef294b903becc5e8b71fa12849
languageName: node
linkType: hard
"hast-util-to-text@npm:^4.0.0":
version: 4.0.2
resolution: "hast-util-to-text@npm:4.0.2"
dependencies:
"@types/hast": "npm:^3.0.0"
"@types/unist": "npm:^3.0.0"
hast-util-is-element: "npm:^3.0.0"
unist-util-find-after: "npm:^5.0.0"
checksum: 10c0/93ecc10e68fe5391c6e634140eb330942e71dea2724c8e0c647c73ed74a8ec930a4b77043b5081284808c96f73f2bee64ee416038ece75a63a467e8d14f09946
languageName: node
linkType: hard
"hast-util-whitespace@npm:^2.0.0":
version: 2.0.1
resolution: "hast-util-whitespace@npm:2.0.1"
checksum: 10c0/dcf6ebab091c802ffa7bb3112305c7631c15adb6c07a258f5528aefbddf82b4e162c8310ef426c48dc1dc623982cc33920e6dde5a50015d307f2778dcf6c2487
languageName: node
linkType: hard
"hastscript@npm:^9.0.0":
version: 9.0.0
resolution: "hastscript@npm:9.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
comma-separated-tokens: "npm:^2.0.0"
hast-util-parse-selector: "npm:^4.0.0"
property-information: "npm:^6.0.0"
space-separated-tokens: "npm:^2.0.0"
checksum: 10c0/66eff826846c55482052ebbc99b6881c14aff6421526634f4c95318ba1d0d1f1bbf5aa38446f388943c0f32d5383fa38740c972b37678dd1cd0c82e6e5807fbf
languageName: node
linkType: hard
"http-cache-semantics@npm:^4.1.1":
version: 4.1.1
resolution: "http-cache-semantics@npm:4.1.1"
@@ -1884,6 +2127,17 @@ __metadata:
languageName: node
linkType: hard
"katex@npm:0.16.21, katex@npm:^0.16.0, katex@npm:^0.16.7":
version: 0.16.21
resolution: "katex@npm:0.16.21"
dependencies:
commander: "npm:^8.3.0"
bin:
katex: cli.js
checksum: 10c0/e2e4139ba72a13f2393308fbb2b4c5511611a19a40a6e39d956cf775e553af3517dbfd0a54477faaf401c923e4654e32296347846b8ff15dfa579f88ff8579bb
languageName: node
linkType: hard
"kleur@npm:^4.1.5":
version: 4.1.5
resolution: "kleur@npm:4.1.5"
@@ -2237,7 +2491,14 @@ __metadata:
languageName: node
linkType: hard
"nth-check@npm:^2.0.1":
"not@npm:^0.1.0":
version: 0.1.0
resolution: "not@npm:0.1.0"
checksum: 10c0/b75d7b2e41d73e2e1cb3327826d53667b41bc6ff7d7ff1d8014ad3bf410d4ecd46f512683b22a4c043e03cbb2b0a483aa69232d4bf9c0e2ee1a9127fe02f047a
languageName: node
linkType: hard
"nth-check@npm:^2.0.0, nth-check@npm:^2.0.1":
version: 2.1.1
resolution: "nth-check@npm:2.1.1"
dependencies:
@@ -2274,6 +2535,15 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^7.0.0":
version: 7.2.1
resolution: "parse5@npm:7.2.1"
dependencies:
entities: "npm:^4.5.0"
checksum: 10c0/829d37a0c709215a887e410a7118d754f8e1afd7edb529db95bc7bbf8045fb0266a7b67801331d8e8d9d073ea75793624ec27ce9ff3b96862c3b9008f4d68e80
languageName: node
linkType: hard
"path-key@npm:^3.1.0":
version: 3.1.1
resolution: "path-key@npm:3.1.1"
@@ -2431,7 +2701,7 @@ __metadata:
languageName: node
linkType: hard
"prismjs@npm:^1.17.1":
"prismjs@npm:1.29.0, prismjs@npm:^1.17.1":
version: 1.29.0
resolution: "prismjs@npm:1.29.0"
checksum: 10c0/d906c4c4d01b446db549b4f57f72d5d7e6ccaca04ecc670fb85cea4d4b1acc1283e945a9cbc3d81819084a699b382f970e02f9d1378e14af9808d366d9ed7ec6
@@ -2455,6 +2725,13 @@ __metadata:
languageName: node
linkType: hard
"property-information@npm:^6.0.0":
version: 6.5.0
resolution: "property-information@npm:6.5.0"
checksum: 10c0/981e0f9cc2e5acdb414a6fd48a99dd0fd3a4079e7a91ab41cf97a8534cf43e0e0bc1ffada6602a1b3d047a33db8b5fc2ef46d863507eda712d5ceedac443f0ef
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@@ -2494,6 +2771,47 @@ __metadata:
languageName: node
linkType: hard
"rehype-katex-svelte@npm:1.2.0":
version: 1.2.0
resolution: "rehype-katex-svelte@npm:1.2.0"
dependencies:
hast-util-from-string: "npm:^2.0.0"
hast-util-select: "npm:^5.0.5"
hast-util-to-string: "npm:^2.0.0"
katex: "npm:^0.16.7"
checksum: 10c0/3b66cbcb1681b9054e86f62771d66b89a2faa491d3e9bb5e0c327d3296b4bd1c64978dce7e38caf2962f2afe04d3e8699f74edcf3b4136ed727a120801b25c68
languageName: node
linkType: hard
"rehype-katex@npm:7.0.1":
version: 7.0.1
resolution: "rehype-katex@npm:7.0.1"
dependencies:
"@types/hast": "npm:^3.0.0"
"@types/katex": "npm:^0.16.0"
hast-util-from-html-isomorphic: "npm:^2.0.0"
hast-util-to-text: "npm:^4.0.0"
katex: "npm:^0.16.0"
unist-util-visit-parents: "npm:^6.0.0"
vfile: "npm:^6.0.0"
checksum: 10c0/73c770319536128b75055d904d06951789d00a0552c11724c0dac2e244dcb21041630552d118a11cc42233fdcd1bfee525e78a0020fde635bd916cceb281dfb1
languageName: node
linkType: hard
"remark-footnotes@npm:2.0":
version: 2.0.0
resolution: "remark-footnotes@npm:2.0.0"
checksum: 10c0/45b55b3440b74bfeed11fba5ed6b31f2fd35ab4e9ba169061b76a19f5ff4d16d851c9f3c423c7fa54eb0fa5e6043b89098cb9478e9b5b417cf4bdef5571b0236
languageName: node
linkType: hard
"remark-math@npm:3":
version: 3.0.1
resolution: "remark-math@npm:3.0.1"
checksum: 10c0/5f2d08d0b8a4eb8926ef25107b00d781d504954fee7e404080907e38e5afa7f6f7a8f1ddb9b554d012130a51125a38751bea80a3313732c36eac82cb0328d4f3
languageName: node
linkType: hard
"resolve@npm:^1.1.7, resolve@npm:^1.22.8":
version: 1.22.10
resolution: "resolve@npm:1.22.10"
@@ -2814,6 +3132,13 @@ __metadata:
languageName: node
linkType: hard
"space-separated-tokens@npm:^2.0.0":
version: 2.0.2
resolution: "space-separated-tokens@npm:2.0.2"
checksum: 10c0/6173e1d903dca41dcab6a2deed8b4caf61bd13b6d7af8374713500570aa929ff9414ae09a0519f4f8772df993300305a395d4871f35bc4ca72b6db57e1f30af8
languageName: node
linkType: hard
"sprintf-js@npm:^1.1.3":
version: 1.1.3
resolution: "sprintf-js@npm:1.1.3"
@@ -3193,6 +3518,44 @@ __metadata:
languageName: node
linkType: hard
"unist-util-find-after@npm:^5.0.0":
version: 5.0.0
resolution: "unist-util-find-after@npm:5.0.0"
dependencies:
"@types/unist": "npm:^3.0.0"
unist-util-is: "npm:^6.0.0"
checksum: 10c0/a7cea473c4384df8de867c456b797ff1221b20f822e1af673ff5812ed505358b36f47f3b084ac14c3622cb879ed833b71b288e8aa71025352a2aab4c2925a6eb
languageName: node
linkType: hard
"unist-util-is@npm:^5.0.0":
version: 5.2.1
resolution: "unist-util-is@npm:5.2.1"
dependencies:
"@types/unist": "npm:^2.0.0"
checksum: 10c0/a2376910b832bb10653d2167c3cd85b3610a5fd53f5169834c08b3c3a720fae9043d75ad32d727eedfc611491966c26a9501d428ec62467edc17f270feb5410b
languageName: node
linkType: hard
"unist-util-is@npm:^6.0.0":
version: 6.0.0
resolution: "unist-util-is@npm:6.0.0"
dependencies:
"@types/unist": "npm:^3.0.0"
checksum: 10c0/9419352181eaa1da35eca9490634a6df70d2217815bb5938a04af3a662c12c5607a2f1014197ec9c426fbef18834f6371bfdb6f033040fa8aa3e965300d70e7e
languageName: node
linkType: hard
"unist-util-remove-position@npm:^5.0.0":
version: 5.0.0
resolution: "unist-util-remove-position@npm:5.0.0"
dependencies:
"@types/unist": "npm:^3.0.0"
unist-util-visit: "npm:^5.0.0"
checksum: 10c0/e8c76da4399446b3da2d1c84a97c607b37d03d1d92561e14838cbe4fdcb485bfc06c06cfadbb808ccb72105a80643976d0660d1fe222ca372203075be9d71105
languageName: node
linkType: hard
"unist-util-stringify-position@npm:^2.0.0":
version: 2.0.3
resolution: "unist-util-stringify-position@npm:2.0.3"
@@ -3202,6 +3565,57 @@ __metadata:
languageName: node
linkType: hard
"unist-util-stringify-position@npm:^4.0.0":
version: 4.0.0
resolution: "unist-util-stringify-position@npm:4.0.0"
dependencies:
"@types/unist": "npm:^3.0.0"
checksum: 10c0/dfe1dbe79ba31f589108cb35e523f14029b6675d741a79dea7e5f3d098785045d556d5650ec6a8338af11e9e78d2a30df12b1ee86529cded1098da3f17ee999e
languageName: node
linkType: hard
"unist-util-visit-parents@npm:^5.1.1":
version: 5.1.3
resolution: "unist-util-visit-parents@npm:5.1.3"
dependencies:
"@types/unist": "npm:^2.0.0"
unist-util-is: "npm:^5.0.0"
checksum: 10c0/f6829bfd8f2eddf63a32e2c302cd50978ef0c194b792c6fe60c2b71dfd7232415a3c5941903972543e9d34e6a8ea69dee9ccd95811f4a795495ed2ae855d28d0
languageName: node
linkType: hard
"unist-util-visit-parents@npm:^6.0.0":
version: 6.0.1
resolution: "unist-util-visit-parents@npm:6.0.1"
dependencies:
"@types/unist": "npm:^3.0.0"
unist-util-is: "npm:^6.0.0"
checksum: 10c0/51b1a5b0aa23c97d3e03e7288f0cdf136974df2217d0999d3de573c05001ef04cccd246f51d2ebdfb9e8b0ed2704451ad90ba85ae3f3177cf9772cef67f56206
languageName: node
linkType: hard
"unist-util-visit@npm:^4.0.0":
version: 4.1.2
resolution: "unist-util-visit@npm:4.1.2"
dependencies:
"@types/unist": "npm:^2.0.0"
unist-util-is: "npm:^5.0.0"
unist-util-visit-parents: "npm:^5.1.1"
checksum: 10c0/56a1f49a4d8e321e75b3c7821d540a45165a031dd06324bb0e8c75e7737bc8d73bdddbf0b0ca82000f9708a4c36861c6ebe88d01f7cf00e925f5d75f13a3a017
languageName: node
linkType: hard
"unist-util-visit@npm:^5.0.0":
version: 5.0.0
resolution: "unist-util-visit@npm:5.0.0"
dependencies:
"@types/unist": "npm:^3.0.0"
unist-util-is: "npm:^6.0.0"
unist-util-visit-parents: "npm:^6.0.0"
checksum: 10c0/51434a1d80252c1540cce6271a90fd1a106dbe624997c09ed8879279667fb0b2d3a685e02e92bf66598dcbe6cdffa7a5f5fb363af8fdf90dda6c855449ae39a5
languageName: node
linkType: hard
"update-browserslist-db@npm:^1.1.1":
version: 1.1.1
resolution: "update-browserslist-db@npm:1.1.1"
@@ -3223,6 +3637,16 @@ __metadata:
languageName: node
linkType: hard
"vfile-location@npm:^5.0.0":
version: 5.0.3
resolution: "vfile-location@npm:5.0.3"
dependencies:
"@types/unist": "npm:^3.0.0"
vfile: "npm:^6.0.0"
checksum: 10c0/1711f67802a5bc175ea69750d59863343ed43d1b1bb25c0a9063e4c70595e673e53e2ed5cdbb6dcdc370059b31605144d95e8c061b9361bcc2b036b8f63a4966
languageName: node
linkType: hard
"vfile-message@npm:^2.0.4":
version: 2.0.4
resolution: "vfile-message@npm:2.0.4"
@@ -3233,6 +3657,26 @@ __metadata:
languageName: node
linkType: hard
"vfile-message@npm:^4.0.0":
version: 4.0.2
resolution: "vfile-message@npm:4.0.2"
dependencies:
"@types/unist": "npm:^3.0.0"
unist-util-stringify-position: "npm:^4.0.0"
checksum: 10c0/07671d239a075f888b78f318bc1d54de02799db4e9dce322474e67c35d75ac4a5ac0aaf37b18801d91c9f8152974ea39678aa72d7198758b07f3ba04fb7d7514
languageName: node
linkType: hard
"vfile@npm:^6.0.0":
version: 6.0.3
resolution: "vfile@npm:6.0.3"
dependencies:
"@types/unist": "npm:^3.0.0"
vfile-message: "npm:^4.0.0"
checksum: 10c0/e5d9eb4810623f23758cfc2205323e33552fb5972e5c2e6587babe08fe4d24859866277404fb9e2a20afb71013860d96ec806cb257536ae463c87d70022ab9ef
languageName: node
linkType: hard
"vite-imagetools@npm:^7.0.1":
version: 7.0.5
resolution: "vite-imagetools@npm:7.0.5"
@@ -3299,6 +3743,13 @@ __metadata:
languageName: node
linkType: hard
"web-namespaces@npm:^2.0.0":
version: 2.0.1
resolution: "web-namespaces@npm:2.0.1"
checksum: 10c0/df245f466ad83bd5cd80bfffc1674c7f64b7b84d1de0e4d2c0934fb0782e0a599164e7197a4bce310ee3342fd61817b8047ff04f076a1ce12dd470584142a4bd
languageName: node
linkType: hard
"web@workspace:apps/web":
version: 0.0.0-use.local
resolution: "web@workspace:apps/web"
@@ -3312,11 +3763,18 @@ __metadata:
"@sveltejs/kit": "npm:2.15.1"
"@sveltejs/vite-plugin-svelte": "npm:4.0.4"
"@tailwindcss/typography": "npm:0.5.15"
"@types/prismjs": "npm:1.26.5"
autoprefixer: "npm:10.4.20"
deepmerge: "npm:4.3.1"
katex: "npm:0.16.21"
lucide-svelte: "npm:0.474.0"
mdsvex: "npm:0.12.3"
misskey-js: "npm:2024.11.1-alpha.0"
prismjs: "npm:1.29.0"
rehype-katex: "npm:7.0.1"
rehype-katex-svelte: "npm:1.2.0"
remark-footnotes: "npm:2.0"
remark-math: "npm:3"
schema-dts: "npm:1.1.2"
svelte: "npm:5.16.1"
svelte-check: "npm:4.1.4"
@@ -3402,3 +3860,10 @@ __metadata:
checksum: 10c0/8f693609c31cbb4449db223acd61661bc93b73e615f9db6fb8c86d4ceea84ca54cbbeebcf53cf74c22a1f923b92abd18e97988a5e175c76b6ab17238e5593a9d
languageName: node
linkType: hard
"zwitch@npm:^2.0.0":
version: 2.0.4
resolution: "zwitch@npm:2.0.4"
checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e
languageName: node
linkType: hard