小規模なSPAをつくってみてわかったFable(F# -> JS)の良いところ
Tags: fsharp, fable, react
Published: 2020-03-29 05:03:03

---
title: 小規模なSPAをつくってみてわかったFable(F# -> JS)の良いところ
published: true
description: F#(Fable)でWebフロントエンド開発をやっていくモチベーションについて、TypeScript/PureScriptと比較しながら考えていく記事です。
tags: FSharp, Fable, React.js
---

突然ですが私、「論文等の書誌引用データのフォーマットを変換するための SPA」をつくりました✌️

![](https://cdn-ak.f.st-hatena.com/images/fotolife/E/E_ntyo/20200320/20200320193040.png)

[RIS形式の書誌引用データをSIST02形式に変換するツールをつくった - いいんちょのブログ](https://e-ntyo.hatenablog.com/entry/2020/03/20/194042)

開発には Fable という AltJS を使ったのですが、Fableは素晴らしいツールであるにもかかわらず、あまりWeb開発者に知られていない印象を受けます。この機会にFableを紹介する日本語記事を書こうと思います💁‍♂️

## Fable とは

> Fable is a compiler powered by Babel designed to make F# a first-class citizen of the JavaScript ecosystem
> https://fable.io/

Fable は F#のコードを Babel compatible な AST に変換するツールです。つまり TypeScript のように、最終的に JavaScript のコードに変換されるプログラミング言語を書くことができます。

F#の良いところについては、[@cannorin](https://github.com/cannorin)さんの「[F# を知ってほしい](https://qiita.com/cannorin/items/59d79cc9a3b64c761cd4)」が詳しいです。

この記事では、Fable の良いところ、すなわち F#を Web フロントエンド開発で使うとどのような点が嬉しいのかなどについて書いていきます。また、Fable というツールそのものの良い点にも触れていきます。

## Fable(F#) のよいところ

Fable でなくても、TypeScript という非常に便利で手軽で強力な AltJS がありますし、他にも Elm や ReasonML、PureScript があります。ただし、私は Elm, ReasonML その他の AltJS による開発の経験がないため、以降でも比較対象には TypeScript と PureScript が使われます。あしからず 🙇‍♂️

私個人が Fable を選んだ理由は、以下の条件を満たす AltJS が欲しかったためです。

1. TypeScript と PureScript の中間くらいの抽象度で
2. TypeScript で普段感じている不満点を解消してくれて
3. ESModules な JS のコードを出力可能で
4. Language Server の実装やいい感じのビルドツールが存在する

### 1. 適当な抽象度

この項目については、私が過去に PureScript を使って SPA を開発した際の経験に基づいています。

PureScript は Row Polymorphism などを実現するための非常に高度な型システムを備えていますが、そのせいか公開されているライブラリを使って開発しようとするとコンパイルを通すことがとても大変で、正直フラストレーションを感じることもありました。

PureScript には TypeScript のような漸進的型付けの仕組みはありませんし、 `tsconfig.json` の `compilerOptions` ようにコンパイル時の設定を細かく設定するということもできないため、これは私にとっては大きな問題でした。むしろ Haskell のバックグラウンドをお持ちの方や、言語仕様の学習にたくさんの時間を確保できる方にとっては非常に良い選択肢だと考えています。

F#にも漸進的片付けの仕組みはありませんが、以下の理由から PureScript のコードを書くときほどの頭の痛さはありませんでした。

- 型クラスがありません
- 型レベルプログラミングを強いられることがほぼありません
  - PureScript のライブラリでは、「[Record の型定義から対応する JSON エンコーダを導出する](https://github.com/justinwoo/purescript-simple-json)」といった型レベルプログラミングを割とバンバンやっています
- デフォルトではモナドがありません
  - [Computational Expression を使うことでやろうと思えば実装できます](https://github.com/dsyme/fsharp-presentations/blob/master/design-notes/ces-compared.md)
    - 実際に[FSharpPlus というライブラリ](https://github.com/fsprojects/FSharpPlus)で提供されていますが、[Fable ではまだ使えないようです](https://github.com/fsprojects/FSharpPlus/issues/243)

### 2. TypeScript で普段感じている不満

私は普段 TypeScript を書いていて、開発体験にある程度満足しているのですが、「もっとこうなってほしい」と思う点がいくつかあります。

1. パターンマッチがほしい
2. Template literals(sprintf)は Type Safe であってほしい
3. 関数合成が手軽にできてほしい

ただし、PureScript や ReasonML でもこれらの不満は解消されます。

#### 2.1 パターンマッチ

まずパターンマッチについてですが、TypeScript でも次のようにして書くことはできます。

```typescript
function maybe<A, B>(
  whenNone: () => B,
  whenSome: (a: A) => B,
  fa: Option<A>
): B {
  switch (fa._tag) {
    case "None":
      return whenNone();
    case "Some":
      return whenSome(fa.value);
  }
}
```

(コードは[fp-ts のドキュメント](https://gcanti.github.io/fp-ts/guides/purescript.html#pattern-matching)内のものです)

ただし、JavaScript における Switch は式ではなく文ですから、パターンマッチ部分の処理を変数に束縛できません。
fp-ts の作者 [@gcanti](https://github.com/gcanti) さんのブログ記事では、もう少しパターンマッチを宣言的に書く方法("'poorman' pattern match")が紹介されています。 `List` では `onNil` と `onCons` 、 `Option` では `onSome` と `onNone` の時に評価される関数を定義して使う、という考え方です。

```typescript
//        ↓ type parameter
type List<A> = { type: "Nil" } | { type: "Cons"; head: A; tail: List<A> };
//                                                              ↑ recursion

const fold = <A, R>(
  fa: List<A>,
  onNil: () => R,
  onCons: (head: A, tail: List<A>) => R
): R => (fa.type === "Nil" ? onNil() : onCons(fa.head, fa.tail));

const length = <A>(fa: List<A>): number =>
  fold(
    fa,
    () => 0,
    (_, tail) => 1 + length(tail)
  );
```

(コードは[Functional design: Algebraic Data Types - DEV Community 👩‍💻👨‍💻](https://dev.to/gcanti/functional-design-algebraic-data-types-36kf)中のものです)

このやり方の欠点は代数的データ型 1 つにつきこういった関数を毎回用意しなければならないということです。fp-ts では `Option` , `Either` などの代数的データ型については `fold` などを提供しており便利ですが、自前で代数的データ型を宣言した際にはやはり対応した `fold` を定義して使うことになります。

一方で、F#は言語仕様としてパターンマッチを定義しているため、自前の代数的データ型についてもふつうにパターンマッチを使うことができます。しかも、以下の `match` は文でなく式です。

```fsharp
[<Literal>]
let Three = 3

let filter123 x =
    match x with
    // The following line contains literal patterns combined with an OR pattern.
    | 1 | 2 | Three -> printfn "Found 1, 2, or 3!"
    // The following line contains a variable pattern.
    | var1 -> printfn "%d" var1

for x in 1..10 do filter123 x
```

(コードは[F#のドキュメント](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/pattern-matching#constant-patterns)内のものです)

#### 2.2 Template literals(sprintf)は Type Safe であってほしい

これは言語仕様というよりはもう少し具体的な、特定の機能についての話ですが、ES2015 specification から導入された Template literals を TypeSafe に扱いたいという思いがあります。

（以下のコードの一部に誤りがあったことを [@otofune](https://github.com/otofune)から教えて頂き、修正しました。）

```typescript
const displayName = "e_ntyo";
const user = { displayName };
const tagged = `Hello! I am ${displayName}`; // "Hello! I am e_ntyo"
const tagged2 = `Hello! I am ${user}`; // "Hello! I am [object Object]" <- 勝手にtoString()されている
```

FSharp ではこの機能は `sprintf` という関数として提供されており、Type-Safe です。

```fsharp
type User = { DisplayName: string }

let displayName = "e_ntyo"
let user = { DisplayName = displayName; }
let tagged = displayName |> sprintf "Hello! I am %s"; // "Hello! I am e_ntyo"
let tagged2 = user |> sprintf "Hello! I am %s"; // The type 'User' does not match the type 'string'
```

#### 2.3 関数合成が手軽にできてほしい

TypeScript では関数合成がやや面倒で、Type-Safe にやっていくためには次のような合成用関数を定義する必要があります。

```typescript
function compose<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {
  return x => g(f(x));
}

interface Person {
  name: string;
  age: number;
}

function getDisplayName(p: Person) {
  return p.name.toLowerCase();
}

function getLength(s: string) {
  return s.length;
}

// has type '(p: Person) => number'
const getDisplayNameLength = compose(getDisplayName, getLength);

// works and returns the type 'number'
getDisplayNameLength({ name: "Person McPersonface", age: 42 });
```

(コードは[TypeScript のドキュメント](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#higher-order-type-inference-from-generic-functions)からです。)

F#では、関数合成用のオペレータが最初から提供されており、より簡潔に記述できます。

```fsharp
// Write code or load a sample from sidebar
type Person = {
    Name: string;
    Age: int;
}

let getDisplayName (p: Person) =
  p.Name.ToLower()

let getLength (s: string) =
  s.Length

let getDisplayNameLength = getDisplayName >> getLength

getDisplayNameLength { Name = "Person McPersonface"; Age = 42 };
```

2 つ以上の関数を合成する場合でも、コードは綺麗です。

```typescript
compose(compose(compose(f, g), h), i);
```

```fsharp
f >> g >> h >> i
```

### 3. target の Module System

AltJS としては、コードを書いている際に、「こういう JS のコードにコンパイルされるのだろうな」というイメージができると理想的です。

例えば、以下の PureScript のコードをトランスパイルするとします。

```haskell
module Main where

import Prelude

import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (log)

f :: Maybe Boolean -> Either Boolean Boolean -> String
f a b = case a, b of
  Just true, Right true -> "Both true"
  Just true, Left _ -> "Just is true"
  Nothing, Right true -> "Right is true"
  _, _ -> "Both are false"

main :: Effect Unit
main = do
  log $ f (Just true) (Right true) -- Both true
```

このような CommonJS モジュールの JS のコードに変換されます。
（※[@oreshinya](https://github.com/oreshinya)さんから「掲載されているコードは CommonJS モジュールのコード（ `purs build` によって生成されるコード）ではなく、 `purs bundle` によって生成されるiife形式のコードになっている」というご指摘を頂き修正いたしました。）

```javascript
// Generated by purs version 0.13.6
"use strict";
var Data_Either = require("../Data.Either/index.js");
var Data_Maybe = require("../Data.Maybe/index.js");
var Effect_Console = require("../Effect.Console/index.js");
var f = function (a) {
    return function (b) {
        if (a instanceof Data_Maybe.Just && (a.value0 && (b instanceof Data_Either.Right && b.value0))) {
            return "Both true";
        };
        if (a instanceof Data_Maybe.Just && (a.value0 && b instanceof Data_Either.Left)) {
            return "Just is true";
        };
        if (a instanceof Data_Maybe.Nothing && (b instanceof Data_Either.Right && b.value0)) {
            return "Right is true";
        };
        return "Both are false";
    };
};
var main = Effect_Console.log(f(new Data_Maybe.Just(true))(new Data_Either.Right(true)));
module.exports = {
    f: f,
    main: main
};
```

一方で、F#ではだいたい同じ処理をこのように書くことができます。

```fsharp
// http://www.fssnip.net/ji/title/Either-in-F
[<AutoOpen>]
module Either

type Either<'a, 'b> =
    | Left of 'a
    | Right of 'b

type either<'a, 'b> =
    Either<'a, 'b> // lower-case alias like option

let isLeft = function
  | Left _ -> true
  | _      -> false

let isRight = function
  | Right _ -> true
  | _      -> false

let f (fa: Option<bool>) (fb: Either<bool, bool>) =
  match fa, fb with
  | (Some true), (Right true) -> "Both true"
  | (Some true), (Left _) -> "Some is true"
  | None, (Right true) -> "Right is true"
  | _, _ -> "Both are false"

f (Some true) (Right true) |> printfn "%s"
```

Fable 用の Webpack の Loader である `fable-loader` などを使うことで、デフォルトの設定では次のような ESModules 形式の JS のコードに変換されます。

```javascript
import { declare, Union } from "fable-library/Types.js";
import { union } from "fable-library/Reflection.js";
import { toConsole, printf } from "fable-library/String.js";
export const Either$00602 = declare(function Either_Either(
  tag,
  name,
  ...fields
) {
  Union.call(this, tag, name, ...fields);
},
Union);
export function Either$00602$reflection($gen$$1, $gen$$2) {
  return union("Either.Either`2", [$gen$$1, $gen$$2], Either$00602, () => [
    ["Left", [$gen$$1]],
    ["Right", [$gen$$2]]
  ]);
}
export function isLeft(_arg1) {
  if (_arg1.tag === 0) {
    return true;
  } else {
    return false;
  }
}
export function isRight(_arg1$$1) {
  if (_arg1$$1.tag === 1) {
    return true;
  } else {
    return false;
  }
}
export function f(fa, fb) {
  var $target$$7;

  if (fa == null) {
    if (fb.tag === 1) {
      if (fb.fields[0]) {
        $target$$7 = 2;
      } else {
        $target$$7 = 3;
      }
    } else {
      $target$$7 = 3;
    }
  } else if (fa) {
    if (fb.tag === 0) {
      $target$$7 = 1;
    } else if (fb.fields[0]) {
      $target$$7 = 0;
    } else {
      $target$$7 = 3;
    }
  } else {
    $target$$7 = 3;
  }

  switch ($target$$7) {
    case 0: {
      return "Both true";
    }

    case 1: {
      return "Some is true";
    }

    case 2: {
      return "Right is true";
    }

    case 3: {
      return "Both are false";
    }
  }
}

(function() {
  const arg10 = f(true, new Either$00602(1, "Right", true));
  const clo1 = toConsole(printf("%s"));
  clo1(arg10);
})();
```

SPA 開発のように生成される JS をブラウザで動かしたい場合、Tree Shaking ができるなどの理由から ESModules 形式で出力したくなるケースがあります。

PureScript は現状 CommonJS Modules 以外のモジュールシステムをターゲットにできず、[ES modules · Issue #3613 · purescript/purescript](https://github.com/purescript/purescript/issues/3613)で今後の対応が議論されています。

また、Fable は仕組みとして `F#のコード -> F# AST -> Babel AST -> JS のコード` の順に変換していくため、Bundler などを使う際に Babel のオプションを噛ませることも可能です。例えば Webpack([Fable/src/fable-loader at master · fable-compiler/Fable](https://github.com/fable-compiler/Fable/tree/master/src/fable-loader)) で Babel の `@babel/preset-env` を使う場合は、次のように `webpack.config.js` を記述します。

```javascript
const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/App.fsproj",
  devtool: "  source-map",
  output: {
    path: path.join(__dirname, "./public"),
    filename: "bundle.js"
  },
  devServer: {
    publicPath: "/",
    contentBase: "./public",
    port: 8080
  },
  module: {
    rules: [
      {
        test: /\.fs(x|proj)?$/,
        exclude: "/node_modules/",
        use: {
          loader: "fable-loader",
          options: {
            babel: {
              presets: [
                [
                  "@babel/preset-env",
                  {
                    modules: false
                  }
                ]
              ]
            }
          }
        }
      }
    ]
  },
  optimization: {
    usedExports: true
  }
};
```

Source Map や Tree Shaking を有効にするための設定が一部含まれています。`fableconfig.json` (TS の `tsconfig.json`に相当) にもそういった設定を記述する必要があります。

```json
{
  "sourceMaps": true,
  "targets": {
    "production": {
      "sourceMaps": false
    }
  },
  "module": "es2015",
  "ecma": "es2015"
}
```

これで TypeScript を使うときと変わらず、Tree Shaking や sourcemap が利用できます。素晴らしいですね。

### 2.4 Language Server の実装やいい感じのビルドツールが存在する

F#にはどちらもあります。ビルドツールは `dotnet` というものがあり、Language Server の実装は Vim や VSCode の拡張から使われています。ナイスなパッケージマネージャの `paket` というものもあります。詳しくは[@cannorin](https://github.com/cannorin)さんの「[F# を知ってほしい](https://qiita.com/cannorin/items/59d79cc9a3b64c761cd4)」をご覧ください。

> - Mono と同じく, .NET Core は Windows, OS X, Linux のどのプラットフォームでも全く同じ開発・実行環境を使うことができます.
> - .NET Core には dotnet という CLI ツール が同梱されており, Rust における cargo コマンドと同様の立ち位置・同等の強力な機能を備えています.
>   - SDK 自体に同梱されているので何もしなくても使えますし, コンパイラを作っているのと同じところが作っているので余計な互換性問題を考えなくて済むのも利点です(cargo と同じように).
>
> ---
>
> Q. Visual Studio Code がないと書けないのでは？
>
> A. Vim プラグイン や Emacs mode があり, IntelliSense 補完やオンザフライでのシンタックス/コンパイルエラーチェック, 定義されているソースへのジャンプなどを使うことができます.
>
> また, Ionide という VSCode 用の F# 拡張機能があり, こちらでは上に加えて CodeLens での型シグネチャ表示やマウスオーバーでの型表示, GUI でのデバッグなどもすることができます.
>
> なお搭載されている補完エンジン自体はすべて共通のもので, Visual Studio のものよりは賢くないですが十分便利です.

## まとめ

- Fable という F#のコードを Babel compatible な AST に変換するツールがある
  - Babel を使うことで TypeScript 同様、target などを自由に指定できる
  - バンドラを使う際にはTree Shakingやsourcemapを利用できる
- Fable によって、F#で Web フロントエンド開発をすることができる
  - Type-Safe で Functional なプログラミングができるようになる
  - [現段階で JavaScript にはない](https://github.com/tc39/proposal-pattern-matching)パターンマッチなどの機能を使うことができる