[F#][小ネタ] レコードリテラルと型注釈

F# のレコードリテラルのちょっとした問題とちょっとした解決策。

問題1: フィールド名が重複しているとき

F# のレコード型を構築する構文では、フィールドの名前から型が推測される。複数のレコード型が同一の名前のフィールドを定義しているとき、そのフィールドは最後に定義されたレコード型のフィールドとみなされる。

例えば REPL (fsharpi) で次のようにすると:

$ fsharpi

// Input
type User =
  {
    Id: int64
    Name: string
  }
;;

type Book =
  {
    Id: int64
    Name: string
  }
;;

{ Id = 1L; Name = "Foo" } ;;

このレコードは Book 型に推論されて、

// Output
val it : Book = {Id = 1L;
                 Name = "Foo";}

となる。

解決1: フィールドを型名で修飾する

この状態で User をインスタンス化するには、いずれかのフィールド名を修飾付きで指定すればいい。

// Input
{ User.Id = 1L; Name = "Foo" } ;;
// Output
val it : User = {Id = 1L;
                 Name = "Foo";}

問題2: レコード型がスコープにないとき

レコード型がスコープに入っていないとき、つまりそのレコード型が定義されている module や namespace を open していないとき、レコードリテラルの構文はかなり冗長になる。

$ fsharpi

// Input
module Types =
  type User =
    {
      Id: int64
      Name: string
    }

  type Book =
    {
      Id: int64
      Name: string
    }
;;

module T = Types;;

{ T.User.Id = 1L; T.User.Name = "Foo" } ;;

注意点は、 T.User.Id を見た時点でレコードの型が決定されるにもかかわらず Name の修飾を省略できないことだ:

// Input
{ T.User.Id = 1L; Name = "Foo" } ;;
// Output
  { T.User.Id = 1L; Name = "Foo" } ;;
  ------------------^^^^

error FS0039: The record label 'Name' is not defined.

解決2: 型注釈をつける

レコードリテラルの型を明示的に指定すると、非修飾でフィールド名を使えるようだ:

// Input
({ Id = 1L; Name = "Foo" }: T.User) ;;
// Output
val it : Types.User = {Id = 1L;
                       Name = "Foo";}

束縛時に型を指定してもよい。型名が前に来るので、こちらのほうが読みやすい気がする:

// Input
let user: T.User = { Id = 1L; Name = "Foo" }
user ;;
// Output
val it : Types.User = {Id = 1L;
                       Name = "Foo";}

さらに、次のように id を経由すると型名とリテラルの近接性がより明確になる:

// Input
id<T.User> { Id = 1L; Name = "Foo" } ;;
// Output
val it : Types.User = {Id = 1L;
                       Name = "Foo";}

これはやりすぎかもしれない、というのも初見では id がなんのためにあるのか分からないからだ。

修飾の強制

レコード型に [<RequireQualifiedAccess>] をつかうと、レコード型をスコープに入れてもフィールド名はスコープに入らなくなる。つまり、前述の冗長な構文を使う必要がある……とずっと思っていたが、「型注釈」の方法であれば問題ない。

この属性をつけておくと、フィールド名が重複するかどうか気にしなくてよくなる。重複したフィールド名が後ろに追加されることでレコードリテラルの型が変わることもなくなる。

[<RequireQualifiedAccess>]
type User =
  {
    Id: int64
    Name: string
  }
;;

({ Id = 1L; Name = "Foo" }: User) ;;

まとめ

  • フィールド名の重複を避けよう。
  • フィールド名が重複しているときは 型名.フィールド名 = ... としよう。
  • レコード型がスコープにないときは ({ ... }: 型名) としよう。

関連記事