F#のFSharp.Data でつまづいたポイント3選

投稿者:

はじめに

7月に下記の集計を出したのですが、その際に書いたプログラムをF#で再実装しました (もともとはPython)。もう少しF#に慣れないとね…という感じで。実際に実装すると、色々つまづいたので、つまづきポイントを整理してみました。整理してみると、つまづいた点はすべてFSharp.Dataに起因するものでした。

実装は https://github.com/todoa2c/SandboxFsharp をご参照ください。

集計結果の記事はこちらをご参照ください

FSharp.Dataの型プロバイダーに渡せる情報は定数のみ

FSharp.Data はCSV, JSON, XMLなど構造的なデータを解釈し、強い型付けをした上で読み書きできる、型プロバイダーという仕組みがあります。今回は、YouTube Data APIをコールするためにJsonProviderを、コメント取得結果を保存するためにCsvProviderを使いましたが、なかなかハマりました。
例えば下記のように、YouTube Data APIのURLを生成し、レスポンスJSONの型情報を事前に構築しようと思ったのですが、 This is not a valid constant expression or custom attribute value というコンパイルエラーが出力されるのです。
原因は、型情報を生成するのだからコンパイル時点で情報が揃っていなければならないのに、実行時にしか揃わない情報(つまり変数)を渡していたからなのですね。

let providerURL =
    $"{url}?key={apiKey}&part=snippet&videoId={videoID}&order=relevance"

type Comments = JsonProvider<providerURL>
// Compile Error: This is not a valid constant expression or custom attribute value

[<Literal>] の評価タイミング

下記のようなサンプル実装を見かけて、なるほど [<Literal>] というものをつければいいのかと思って試したのですが、コンパイルエラーになる。
これも同様に、 [<Literal>] 属性は、「コンパイル時点で値が確定している定数」であることを示す属性だそうです。そのため、下の実装はコンパイル時点で定数化できないため、コンパイルエラーになった、というものです。

詳細は「名前付きリテラル」をご参照ください https://docs.microsoft.com/ja-jp/dotnet/fsharp/language-reference/literals#named-literals

let [<Literal>] ProviderURL = "https://~~~/***.json"
type Comments = JsonProvider<ProviderURL> 
// Compiles OK

let [<Literal>] providerURL =
    $"{url}?key={apiKey}&part=snippet&videoId={videoID}&order=relevance"
// Compile Error

コンパイル時の実行フォルダの違い

F# Interactiveは手元でF#のコードを素早く解釈、実行させる便利なしくみです。
下記のようなコードを即座に実行し、 Comments を使った操作も試すことができます。

type Comments = JsonProvider<"sampleResponse.json">;;

上記の実装を状態で、いざ本格的に実行するべく上記コードが書かれたGetComments.fsx ファイルを実行しようとしたところ、またエラーが発生しました。F# Interactiveでは間違いなくあったはずのsampleResponse.json が見つからないというのです。

$ dotnet fsi GetComments.fsx

/Users/***/dev/SandboxFsharp/GetComments.fsx(22,17): error FS3033: 型プロバイダー 'ProviderImplementation.JsonProvider' がエラーを報告しました: Cannot read sample JSON from 'sampleResponse.json': Could not find file '/var/folders/7x/tg7wl1hn52b964mmqbhsxf1r0000gn/T/nuget/94109--103437b7-dfa8-4b2d-981c-3c0019634daa/sampleResponse.json'.

パスを見ると、見慣れぬ一時フォルダ的なパスにあるかのような振る舞いをしています。原因までは分からないのですが、察するに型を生成するための一時フォルダ上で処理しているときに、該当のJSONファイルまでは持ってきておらず、相対パスしか指定していないJSONファイルを見つけられずに失敗、ということでしょうか。あくまで推測ですけど。
この場合、下記のような実装に変更すればいいそうです。GetComments.fsx のあるディレクトリからの相対パスも、これで設定できて安心です。
なお、 System.Environment.CurrentDirectory も現在の実行ディレクトリを取ることができ、意味的にはほぼ同じように見えるのですが、こちらはコンパイル時ではなく実行時に決まる定数のため、JsonProviderには利用できませんでした。

type Comments = JsonProvider<"sampleResponse.json", ResolutionFolder=__SOURCE_DIRECTORY__>;;

まとめ

私のようなF#初心者がFSharp.Dataというか型プロバイダーに惹かれ、トライする人がいるかもしれません。私もえらい沢山つまづきました。同じくつまづいた人が、このページを見て次のステップに進むことができたら嬉しいです。