LoginSignup
2
0

More than 3 years have passed since last update.

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

Posted at

1. 概要

10文字未満の文字しか入らないstring型とかあると嬉しい。"F# for fun and profit"の記事: "ここ" とか "ここ" でとりあげられたけど、それでも要望が多すぎて "gistでプロトタイプ" まで追加で作られている議題について。

2. 何が嬉しいか?何が問題か?

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

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

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

3. 概要

緩和策と本格対策の二つがある

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

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

4. 緩和策

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

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

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

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は普通の型なのでこれを使ってより複雑な型を作るのは簡単:

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でプロトタイプ" の方法:

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

// 似たような型 (例: 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

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

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;}

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

// 年齢を一つインクリメントする関数を定義する
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でプロトタイプ" を使うと上手く定義できる。

一方、10文字以内型20文字以内型 など似たような型を量産するTipsはgistに記載がなく 元記事 の方が詳しい(インターフェースを使う)。また、apply などのユーティリティも元記事の方が詳しく記載されている。

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0