[F# Tips] 10文字未満しか入らない文字列型

# 1. 概要
10文字未満の文字しか入らない`string`型とかあると嬉しい。"F# for fun and profit"の記事: ["ここ"](https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/) とか ["ここ"](https://fsharpforfunandprofit.com/posts/designing-with-types-more-semantic-types/) でとりあげられたけど、それでも要望が多すぎて ["gistでプロトタイプ"](https://gist.github.com/swlaschin/54cfff886669ccab895a) まで追加で作られている議題について。

# 2. 何が嬉しいか？何が問題か？
**うれしい点**: 型に「妥当な値を持つことが保証される」という性質が付随していれば、バリデーションが不要になるので 不正値の処理ロジック や関連するバグがなくなる。

**実現を阻む課題**: レコード型等は（ユーザが定義可能な）コンストラクタを経由することなしに作れてしまうため、バリデーションする機会がない。

```fsharp
type Age = Age of int // 負数を入れたくない型があったとしても
let a = Age -5 // もちろん作れてしまう
```

# 3. 概要
緩和策と本格対策の二つがある

- **緩和策**: バイパス可能なので 完全に安全とは言えないが、普通に使える
- **本格対策**: 完全に安全だが、取り回しがめんどくさい

緩和策は 4章で扱い、本格対策は 5章で扱う。緩和策は["F# for fun and profit"の連載で推奨されていた方法](https://fsharpforfunandprofit.com/posts/designing-with-types-more-semantic-types/)で、「ファクトリ関数経由で作るような心がけをしよう」という 規約的な解決案。 さて、もう一方の本格対策は「型情報を隠す。出し入れ・操作のインターフェースを使え」というもので["gistでプロトタイプ"](https://gist.github.com/swlaschin/54cfff886669ccab895a)が公開されている案。


# 4. 緩和策
**プログラマ全員がファクトリ関数で作ればいい**。バリデーションはそこで実施すればよいし、一旦作成されればイミュータブルなので、問題ない。ファクトリ関数をバイパスすれば不正な値は入れられるが 緩和策としては及第:

```fsharp
module Age =
  type T = Age of int
  let create n = if n >=0 && n <= 150 then Some (Age n) else None
```

`T`という変な名前の型にすることで「直接作成してはダメだよ」という意思表示をしている。`create`と名称された関数経由で値を作る。無理やり作ろうと思えば作れてしまう点はご愛敬。使い方の例:

```fsharp
Age.create 10 // val it : Age.T option = Some (Age 10)
Age.create -5 // val it : Age.T option = None
Age.Age -5 // やっちゃダメ！！ val it : Age.T = Age -5
```

`Age.T`は普通の型なのでこれを使ってより複雑な型を作るのは簡単:

```fsharp
type Person = { name: string ; age: Age.T }

let createPerson name n =
  match Age.create n with
  | Some age -> Some { name = name ; age = age }
  | None -> None

createPerson "alice" 13
// val it : Person option = Some {name = "alice"; age = Age 13;}
createPerson "bob" -4
// val it : Person option = None
```



## 5. 本格対策
F# fun and profit の人が作った ["gistでプロトタイプ"](https://gist.github.com/swlaschin/54cfff886669ccab895a) の方法:

DU を `private` にするとファクトリ関数の使用を強制できる。ただし**代償として型が隠蔽される**。そのため ファクトリ関数`create`と取り出し関数`value`が必須になる。実用上は 継続付き取り出し関数`apply`  などなどのインターフェースを付加することがある（後述）。

```fsharp
// 似たような型 (例: 10文字以内型 と 20文字以内型)でロジックを共有したりするために
// 値制約付きの型をまとめて定義すると便利 (ここではやっていない)
module ConstrainedTypes =
  open System
  // 0以上150以下の整数値 (注: nullは 仕様として int型に入らない)
  type IntUpTo150 = private IntUpTo150 of int
  module IntUpTo150 =
    let create n = if n >= 0 && n <= 150 then Some (IntUpTo150 n) else None
    let value (IntUpTo150 n) = n
  // nullでなく 1文字以上10文字以下の 文字列
  type String10 = private String10 of string
  module String10 =
    let create str = 
      if String.IsNullOrEmpty(str) then None
      elif (String.length str > 10) || (str = "") then None
      else Some (String10 str)
    let value (String10 str) = str
```

これをもとにより複雑な型を定義するのも簡単だし、作成も簡単:


```fsharp
open ConstrainedTypes

type Person = { name: String10 ; age: IntUpTo150 }

let createPerson str num =
  match (String10.create str), (IntUpTo150.create num) with
  | Some name, Some age -> Some { name = name ; age = age }
  | _, _ -> None
// 使い方
createPerson "alice" 13 |> Option.get
// val it : Person = {name = String10 "alice"; age = IntUpTo150 13;}
```

型が隠蔽されているので、値の取り出しや各種処理がめんどくさい。それを緩和するために`apply`や`createWithCont`などのユーティリティを生やすこともある:


```fsharp
// 年齢を一つインクリメントする関数を定義する
let tryIncrAge { name=name ; age=age } =
  let nextAge = 1 + (IntUpTo150.value age)
  createPerson (String10.value name) nextAge

createPerson "alice" 13
|> Option.bind tryIncrAge
// val it : Person option = Some {name = String10 "alice"; age = IntUpTo150 14;}

createPerson "bob" 150
|> Option.bind tryIncrAge // None

createPerson "Chaaaaariiieeee" 21
|> Option.bind tryIncrAge // None
```

# 6. 補足
既に示したように  ["gistでプロトタイプ"](https://gist.github.com/swlaschin/54cfff886669ccab895a) を使うと上手く定義できる。

一方、`10文字以内型`と`20文字以内型` など**似たような型を量産するTipsはgistに記載がなく** [元記事](https://fsharpforfunandprofit.com/posts/designing-with-types-more-semantic-types/) の方が詳しい（インターフェースを使う）。また、`apply` などのユーティリティも元記事の方が詳しく記載されている。

なので gist で安全な作り方を学ぶだけでなく、元記事の方で実践を学ぶことも必要だと思う。