F#+WCFでRESTfulなサービスをこしらえる・自分で定義した判別共用体で遊ぶ

自分で定義した判別共用体も使いたいさ


昨日はoption型を扱える事を確認しましたが、optionだけだと物足りないさ。
自分で定義した判別共用体も使いたいさ。
そりゃ使えないよりは使えた方がいい。役に立つか否かは別として、使わない機能でも盛りだくさんなら嬉しくなるものです。

今日のコードはこんな感じです。

namespace FSharpWCF  
  
open System  
open System.Runtime.Serialization  
open System.ServiceModel  
open System.ServiceModel.Web

type Card =
     | Jack
     | Queen
     | King
     | Num of string

[<ServiceContract>]  
type ICardService = interface
  [<OperationContract>]  
  [<WebGet(UriTemplate="card/{id}")>]  
  abstract GetCard : id:string -> Card
  
end  
  
type CardService() =
  interface ICardService with  
    member this.GetCard id =
      match id.ToLower() with
      | "jack" -> Jack
      | "queen" -> Queen
      | "king" -> King
      | _ -> Num "1"

トランプの数字(1〜13)を("1"〜"10", Jack, Queen, King)と定義した判別共用体Cardを用います。
(1から10までのNum をstringにしたのは僕が楽チンをするためです)
configファイルは今までのエントリを見て適当に編集してください。


GetCardの返り値がCard型ですが、このCard型はDataContract属性指定をしていません。
この状態で動くでしょうか。

http://localhost:8080/FSharpWCF/CardService/card/jack

{"_tag":0}

おお、返って来ました。こうなるともちろん、
http://localhost:8080/FSharpWCF/CardService/card/queen

{"_tag":1}

http://localhost:8080/FSharpWCF/CardService/card/king

{"_tag":2}

となります。


ですが、
http://localhost:8080/FSharpWCF/CardService/card/1
にアクセスすると(Num 1 を返すパス)

要求エラー

要求の処理中にサーバーでエラーが発生しました。詳細については、サーバー ログを参照してください。

エラーが返って来ました。


C#で作ったクライアントでGetCard("1")にアクセスすると、このような例外が発生します。

GetCardの返り値はCard型なので、C#の方から定義を覗いてみると、こんなソースがこんにちわ!します。

#region アセンブリ {620DF1F0-5FAF-4290-9850-EC9E5C93E634}, v4.0.30319
// {620DF1F0-5FAF-4290-9850-EC9E5C93E634}
#endregion

using Microsoft.FSharp.Core;
using System;
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace FSharpWCF
{
[Serializable]
[DebuggerDisplay("{__DebugDisplay(),nq}")]
[CompilationMapping(SourceConstructFlags.SumType)]
public class Card : IEquatable<Card>, IStructuralEquatable, IComparable<Card>, IComparable, IStructuralComparable
{
[CompilerGenerated]
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public bool IsJack {get; }
[CompilerGenerated]
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public bool IsKing {get; }
[CompilerGenerated]
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public bool IsNum {get; }
[CompilerGenerated]
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public bool IsQueen {get; }
[CompilerGenerated]
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public static Card Jack {get; }
[CompilerGenerated]
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public static Card King {get; }
[CompilerGenerated]
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public static Card Queen {get; }
[CompilerGenerated]
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public int Tag {get; }

[CompilerGenerated]
public override sealed int CompareTo(Card obj);
[CompilerGenerated]
public override sealed int CompareTo(object obj);
[CompilerGenerated]
public override sealed int CompareTo(object obj, IComparer comp);
[CompilerGenerated]
public override sealed bool Equals(Card obj);
[CompilerGenerated]
public override sealed bool Equals(object obj);
[CompilerGenerated]
public override sealed bool Equals(object obj, IEqualityComparer comp);
[CompilerGenerated]
public override sealed int GetHashCode();
[CompilerGenerated]
public override sealed int GetHashCode(IEqualityComparer comp);
[CompilationMapping(SourceConstructFlags.UnionCase, 3)]
public static Card NewNum(string item);

[Serializable]
[DebuggerTypeProxy(typeof(Num@DebugTypeProxy))]
[DebuggerDisplay("{__DebugDisplay(),nq}")]
public class Num : Card
{
[CompilationMapping(SourceConstructFlags.Field, 3, 0)]
[CompilerGenerated]
[DebuggerNonUserCode]
public string Item {get; }
}

public static class Tags
{
public const int Jack = 0;
public const int King = 2;
public const int Num = 3;
public const int Queen = 1;
}
}
}

コードからJack, Queen, Kingがそれぞれ1,2,3に対応している事は分かりました。
型指定のないJackなどがうまく言っているのは内部でintに変換されているからでしょうね。
それならenumと変わらんのや!!!!
エラーが起こるぶんenumより悪くなっとるんや!!!!

でも Num(1) とかも扱いたいです。
やっぱりDataContract指定してないのがいけないのかな?と思ってこんな事をしてみましたが、

(* 思ったように動かないし惜しくもないコード *)
[<DataContract>]
type Card =
     | [<DataMember>] Jack
     | [<DataMember>] Queen
     | [<DataMember>] King
     | [<DataMember>] Num of string

ビックリするほど全然だめでした。

Getもダメなら当然Postもダメだ


Responseとしての判別共用体がエラーを起こす可能性があることは確認出来たので、じゃあRequestならどうかなーって思いました。
結果から言うと一緒でした。


準備としてこんな感じでPOSTオペレーションを実装しました。

[<ServiceContract>]  
type ICardService = interface
  [<OperationContract>]  
  [<WebGet(UriTemplate="card/{id}")>]  
  abstract GetCard : id:string -> Card

  [<OperationContract>]  
  [<WebInvoke(Method="POST", UriTemplate="card")>]  
  abstract PostCard : data:Card -> string
  
end  
  
type CardService() =
  interface ICardService with  
    member this.GetCard id =
      match id.ToLower() with
      | "jack" -> Jack
      | "queen" -> Queen
      | "king" -> King
      | _ -> Num "1"
    member this.PostCard data =
      match data with
      | Jack -> "jack"
      | _ -> "other"

最初、abstract PostCard : data:Card -> string を abstract PostCard : Card -> string と書いていてしばらくはまりました。
Request のメッセージには名前がいるってことでしょうか。


そしてC#クライアントをこんな感じで実装します。

    private void button1_Click(object sender, EventArgs e)
    {
      var baseuri = new Uri("http://localhost:8080/FSharpWCF/CardService/");
      using (var f = new WebChannelFactory<ICardService>(new WebHttpBinding(), baseuri))
      {
        var service = f.CreateChannel();

        var message = service.PostCard(Card.Jack);
        Console.WriteLine(message);

        var message2 = service.PostCard(Card.NewNum("2"));
        Console.WriteLine(message2);
      }
    }

大方の予想通り、service.PostCard(Card.Jack); はうまく動作します。やっぱり内部で暗黙的にintになっているのでしょう。
さあ、service.PostCard(Card.NewNum("2")); がどうなるかと言うと、

シリアライズできねーっつーの!」と怒られました。これも何となく予想はついていましたがやっぱり予想通りです。

判別共用体は諦めるしかないのか?


「じゃあじゃあ判別共用体は使えないの!?」と世の判別共用体大好きっこ達は大変焦る事でしょう。

今の問題は「サービス外部とのメッセージに判別共用体を含めるとエラーが起こる」という事であって、内部で使う分には問題ないでしょう。

そうすると考えられるのは「外部とやり取りするメッセージコントラクト(データコントラクト)と判別共用体とを相互変換する」ということになります。

色々難しい事は考えずに実装するとこんな感じでしょうか。

module CardUtil =
  type Card =
     |Jack
     | Queen
     | King
     | Num of string
  with
    member this.ToStringExt = match this with
                              | Num(x) -> x
                              | Jack -> "Jack";
                              | Queen -> "Queen"
                              | King -> "King"

  let ToCard x = match x with
                    | "Jack" -> Jack
                    | "Queen" ->  Queen
                    | "King" ->  King
                    | "1" | "2" | "3" | "4" | "5"
                    | "6" | "7" | "8" | "9" | "10" -> Num(x)
                    | _ -> raise (WebFaultException(System.Net.HttpStatusCode.NotFound))

[<DataContract>]
type CardData(n) =
  let mutable card = CardUtil.ToCard(n)
  [<DataMember(Name = "Card")>]
  member x.myCard with get() = card.ToStringExt
                  and set(value) = card <- CardUtil.ToCard(value)

[<ServiceContract>]  
type ICardService = interface
  [<OperationContract>]  
  [<WebGet(UriTemplate="card/{id}")>]  
  abstract GetCard : id:string -> CardData

  [<OperationContract>]  
  [<WebInvoke(Method="POST", UriTemplate="card")>]  
  abstract PostCard : data:CardData -> string
  
end  
  
type CardService() =
  interface ICardService with  
    member this.GetCard id =
      new CardData(id)
    member this.PostCard data =
      data.myCard

自己再帰型の判別共用体(木構造とか)作ってしまうと外部とのやり取りは頭を捻ることになるでしょうか。

まとめ

  • 判別共用体をデータコントラクトとして思い通りに動かす方法は分からなかった
  • 判別共用体をメッセージコントラクトに含めるとエラーが起こる可能性がある
  • 内部で使う分には問題ないので、サービス外部とやり取りする時は別のデータコントラクトに(から)変換するに必要がある
  • そこまでして判別共用体を使うべきかどうかは分からないけど、まあ嬉しいし