FAKE4からFAKE5への移行手順

6月6日にFAKE5がリリースされました。

この記事は、手元のプロジェクトをFAKE4からFAKE5に移行した時の記録です。 FAKE4ユーザー視点の内容が多いです。

FAKE5のドキュメント

英語しか無いですが、公式ドキュメントのチュートリアルを読めば大体のことは書いてあります。

https://fake.build/

FAKE4を使っている人は、マイグレーションガイドも読みましょう。

FAKE5の変更点

まずはリリースノートを読んでみます。 FAKE4を使っている人にとって特に重要なのがこれらの更新です。

  • Deep integration into the .NET SDK and .Net Core
  • The old runner (FAKE nuget package) is obsolete
  • FakeLib and FAKE.Deploy are obsolete
  • Clean and modularized API - A lot of stuff is obsolete now as it moved to a different location and will be removed with version 6

要約すると、「APIを刷新しました。FAKE6までに移行してください」ということです。

新しいAPIのデザイン

Legacy FAKEではAutoOpenを多用しているために不要なモジュールまでopenされており、個人的に不満に思ってました。 New FAKEではAutoOpenを使わなくなったので、必要なモジュールだけ使えるようになりました。 とても良い変更だと思いますが、パッケージまで細かく分かれるとは思ってませんでした。やりすぎなんだよなあ。

関数の命名規則も変わりました。 Legacy FAKEには「タスクとなるような関数は大文字で始まる。オプションを表現する関数は小文字で始まる」みたいなルールがありましたが、「小文字で始まる」に統一されたみたいです。

実行方法の変更

実行方法がdotnet cli-toolに変わりました。 特に気をつけることは、引数の指定方法が変わったことです。

CLI Migrationによると、 Legacy FAKEではfake.exe build.fsx target=Releaseや、fake.exe build.fsx Releaseという風にターゲットを指定していましたが、 New FAKEではfake.exe build --target Releaseと指定します。 --targetを省略してfake.exe build Releaseとすると、Releaseがターゲット名と認識されず、正しく実行できません。

組み込みPaket

PaketがFAKEに組み込まれています。そのため、FAKE単体で動作するようになります。 組み込みのPaketを使うには、ビルドスクリプトに次のように記述します。

#r "paket:
nuget Fake.Core.Target"
#load "./.fake/build.fsx/intellisense.fsx"

#r "paket: ..."で、ビルドスクリプトで使用するパッケージ依存を記述できます。 #load "./.fake/build.fsx/intellisense.fsx"IDEでビルドスクリプトを編集するために必要です。 intellisense.fsxの中身は、Paketで参照しているアセンブリをロードする記述があるだけです。

もちろん外部のPaketも利用できて、paket.dependenciesが既にある場合は次のように記述します。

#r "paket: groupref Build"
#load "./.fake/build.fsx/intellisense.fsx"

ただ、この#r "paket: ..."はfsxファイルのFAKE拡張なので、Visual Studioで開くと警告が出ます。 警告を防ぐには、次のように記述します。

#if FAKE
#r "paket: ..."
#endif
#load "./.fake/build.fsx/intellisense.fsx"

またVisual Studioの問題で、netstandardの参照を追加する必要がある場合があります。

最終的に、現状では次のようにするのが良さそうです。

#if FAKE
#r "paket: ..."
#else
#r "netstandard"
#endif
#load "./.fake/build.fsx/intellisense.fsx"

Legacy FAKEとNew FAKEパッケージの見分け方

Legacy FAKEとNew FAKEのNuGetパッケージは完全に別れており、互換性はありません。すべて大文字のFAKEが含まれるパッケージがLegacy FAKEで、先頭だけ大文字のFakeが含まれるパッケージがNew FAKEです。

移行手順

実際にFAKE4からFAKE5に移行した時の手順を書いていこうと思います。 基本的にはマイグレーションガイドの通りですが、一部うまくいかないところもありました。

想定するプロジェクトは次のとおりです。

  • FAKE4を使っている
  • Paketを使っている
  • FAKEがインストールされていない環境(例えばCI)でも、自動でFAKEをインストールしてビルドできるようにしたい

FAKE.Lib を更新してAPIの変更点を洗い出す

FAKE5にもLegacyなAPIは"FAKE.Lib"パッケージとして残されています。"FAKE.Lib"のバージョンを5.0.0以降に更新します。 更新すると、LegacyなAPI全てにObsoleteAttributeが付与されているので、警告がでます。 警告メッセージに移行先のモジュールが書かれているので、覚えておきます。どの関数に移行したかまでは書かれていません。

FAKE5のdotnet cli-toolとテンプレートをインストールする

Getting Startedに書かれている方法で、FAKE5とテンプレートをインストールします。 次のコマンドでインストールします。

dotnet tool install fake-cli-g
dotnet new -i "fake-template::*"

FAKEのbootstrapをプロジェクトにインストールする

この作業はCIでビルドしないのなら必要ないです。

  1. build.fsxとpaket.depencenciesを削除する

    bootstrapをインストールするときにこの2つがあると実行できません。 あとでgitを使って復元します。

  2. dotnet new fake --bootstrap projectを実行する

    build.fsxbuild.projfake.cmdfake.shpaket.dependenciesが新しく作られます。

  3. build.fsxとpaket.depencenciesを復元する

    git checout -- build.fsx paket.dependencies

これで、リポジトリのclone後にfake.cmdを実行すると、FAKEがインストールされるようになります。 bootstrapの原理はfake.cmdbuild.projの中身を見てください。

ビルドスクリプトをFAKE5に移行する

paket.dependenciesのBuildグループを書き換える

とりあえず最小の構成にします。必要なパッケージは後で追加します。

group Build
  framework: netstandard2.0
  source https://nuget.org/api/v2

  nuget Fake.Core.Target

ProjectScaffoldを使っている場合は、github fsharp/FAKE modules/Octokit/Octokit.fsxも削除します。

build.fsxにPaket関係を追記する

#if FAKE
#r "paket: groupref Build"
#else
#r "netstandard"
#endif
#load "./.fake/build.fsx/intellisense.fsx"

この時点では、まだ./.fake/build.fsx/intellisense.fsxは存在しません。

intellisense.fsxを生成する

ビルドスクリプトfake.exeに読み込ませると、intellisence.fsxが更新されます。

スクリプトのターゲットを実行せずに、ただ読み込ませるだけなら、次のコマンドを実行します。

fake.exe build --help

build.fsxの旧APIを新APIに移行する

先程洗い出した変更点の警告に従って、

  1. paket.dependencies にパッケージを追加
  2. intellisense.fsx を再生成
  3. build.fsx を編集

を繰り返していきます。

APIの移行先のモジュール名とパッケージ名は一致しています。 "Use Fake.DotNet.MSBuild instead"という警告が出た場合は、"Fake.DotNet.MSBuild"パッケージを追加して、"Fake.DotNet"名前空間をopenすると新APIを利用できます。

動作確認

動作確認して無事動いたら終了です。

自分の場合は、FAKE.PersimmonがFAKE5に対応していないので、同等のモジュールを自作することになりました。 他は特に問題なく動作しました。

ProjectScaffoldの状況

現在ProjectScaffoldはFAKE5に移行中です。急いでFAKE5に移行する必要もないので、ProjectScaffoldを使っている人は待ってみるのも良い選択だと思います。

Equality 制約

github.com

の実装のためのメモ(宣伝)。

型制約は F# における型安全をマスターするためには必須の知識であるため、細かい仕様も見落としてはならない。

型制約は F# 言語仕様書 4.0 の 5.2 Type Constraints に記述されている。

Equality制約

Equality制約は滅多に使用しないが、かなり緩い制約になっている。

標準ライブラリでは(=)(<>)hashにEquality制約がついている。

定義

こう書く。'aにEquality 制約を指定できる。

<'a when 'a : equality>

挙動

Equality制約はほとんどの型が満たす。

なぜならEqualsメソッドのデフォルトであるSystem.Object.Equalsがリファレンスを比較するようになっているため、 全てのオブジェクトはEqualisy制約を満たす事になっている。

このコードで確認できる。

type A() = class end
let test<'a when 'a : equality> (_: 'a) = ()
do test(A())

AIEquatableを実装しないため、Equality制約を満たさないと予想するかもしれない。 実際には前述のとおり、Equality制約を満たす。

Equality制約を違反する型を定義するには、NoEqualityAttirbuteを型に付ける。

[<NoEquality; NoComparison>]
type B() = class end
do test(B()) // 型 'B' は 'NoEquality' 属性があるため、'equality' 制約をサポートしません

NoEqualityAttributeCustomEqualityAttributeはComparison制約系の属性NoComparisonAttributeCustomComparisonAttributeと組み合わせて使用する。 CustomEqualityAttributeを指定した場合はObject.Equals(obj)IEquatable<_>IStructualEquatableのどれかを実装しなければならない。

Equality制約を満たす条件は次の通り。1、2は言語仕様書に定義されている。3、4は実装を読んだ。

  1. NoEqualityAttributeを持っていない型
  2. 依存する型が全てEquality制約を満たす型
    1. レコード、判別共用体、構造体の型パラメータ、フィールドに使用される型すべて(クラスは対象外)
    2. System.Tupleの要素の型
    3. ジェネリック型のEqualityConditionalOnAttributeが指定されたパラメータ
  3. 関数FSharpFuncではない型(つまりFuncActionはEquality制約を満たす)
  4. CustomEqualityAttributeが指定された型

C# で定義された型や、.Net標準ライブラリの型はEquality制約を満たすと言える。

わかりにくい項目の補足をする。

ジェネリック型のEqualityConditionalOnAttributeが指定されたパラメータ

クラスの型パラメータにEquality制約を満たさない型を指定しても、Equality制約を満たすと判断される。

type C<'a>() = class end
do test (C<int -> int>())

型パラメータにEqualityConditionalOnAttributeを付けると、Equality制約の解決に使用されるようになる。

type C'<[<EqualityConditionalOn>]'a>() = class end
do test (C'<int -> int>()) // 型 '(int -> int)' は関数型なので、'equality' 制約をサポートしませ

CustomEqualityAttributeが指定された型

レコード、判別共用体、構造体に使う。依存する型がEquality制約を満たさなくても良くなる。

[<CustomEquality; NoComparison>]
type D<'a> = {
  Field: 'a
}
with
  override this.Equals(_) = true
  override this.GetHashCode() = 0

let f = fun () -> ()
do test({ Field = f })

EqualityConditionalOnAttributeと同時に使われた場合は、EqualityConditionalOnAttributeも考慮する必要がある。

次のような型を定義した場合は、

[<CustomEquality; NoComparison>]
type E<[<EqualityConditionalOn>]'a, 'b> = A of 'a | B of 'b
with
  override this.Equals(_) = true
  override this.GetHashCode() = 0

E<int, int -> int>という型はEquality制約を満たすが、E<int -> int, int -> int>'aが関数であるためEquality制約を満たさない。

まとめ

実装するのがツライ

リンク

Equality and Comparison Constraints in F# | Don Syme's WebLog on F# and Related Topics

使いたくない型を見えなくする #FsAdvent

この記事はF# Advent Calendar 2015 - connpassの2日目です。

同僚へのイタズラネタを紹介したいと思います。 AutoOpenと組み合わせて混乱に陥れてやりましょう。 標準ライブラリの中には、古くなったり、挙動が紛らわしいという理由で使うべきではないクラスや関数があります。 どこかのサイトから当時は適切だった古いコードを参考にしてきて、うっかり使ってしまう人もいるでしょう。

こんな困った人のコードはコンパイルエラーにしてやる!

一部のメンバのみ制限する

例えば、System.Text.Encoding.UTF8プロパティとSystem.Text.UTF8Encodingクラスの2つで、BOMがどう扱われるか直ぐに言えますか? System.Text.Encoding.UTF8はBOMが付きます。一方、System.Text.UTF8EncodingコンストラクタでBOMの有無を指定できます。 BOMを扱わないシステムを扱っている場合は、System.Text.Encoding.UTF8をなるべく使ってほしくないですね。

module System.Text.Encoding
let UTF8 = ()

このようなモジュールを定義すると、System.Text.Encoding.UTF8の定義を上書きできます。 そして、System.Text.Encoding.UTF8を使おうとすると型が合わずコンパイルエラーになってしまいます。

コンパイルエラーがやり過ぎだと思ったら、ObsoleteAttributeを付けて警告にもできます。

module System.Text.Encoding
let [<System.Obsolete>]UTF8 = System.Text.Encoding.UTF8

この方法はUTF8プロパティのみ上書きされ、他のメンバは普段どおり使用できます。

使ってほしくないクラス、モジュールを制限する

次の例は、.NetにはXMLを扱うには2つの方法があります。 古くからあるSystem.Xml名前空間と、Linq to XmlSystem.Xml.Linq名前空間の2つです。 System.Xml名前空間にあるクラスは使いづらいので、プロジェクト全体で使わせたくない上に、何も指示しないとSystem.Xmlのクラスを使う人までいます。

使わせたくない!コードレビューの時では遅いのだ!

先程は同名のモジュールを定義して、同名のメンバを定義しましたが、今度は同名のクラスを定義します。

namespace System.Xml
type XmlDocument private() = class end

もともとのSystem.Xml.XmlDocumentにアクセスしようと思っても、上記の定義が邪魔をして使えなくなってしまいます。

open System.Xml
let x = XmlDocument() (* この型にアクセスできるオブジェクト コンストラクターはありません *)

めでたしめでたし。

まとめ

こういうのはLintの仕事だと思いました。

モジュールのアクセシビリティ管理を手抜きする

F# には、モジュール内の要素(型、関数)のアクセシビリティを指定するには、2つの方法があります。

  1. シグネチャファイルを使う
  2. 非公開要素毎に internal / private を指定する

シグネチャファイルは、型名のみ公開しメンバは隠すなど、柔軟にアクセシビリティを指定できます。しかし、実装とシグネチャが別ファイル、同期が大変、という辛さがあり、手軽には使えません。

また、非公開要素毎に internal / private を指定する方法は、要素数が多くなれば指定に手間がかかる、ファイルを見ただけでは公開メンバの一覧が分からない問題があります。

シグネチャファイルを使うまでもなく、手軽にアクセシビリティを制御したい場合は、こう書くと良いんじゃないかなぁと思います。

module Hoge

(* 公開したい型はここに定義する *)

module internal Impl =
  (* 実装に必要な関数、型をImplモジュールに押し込む *)

  let f x = ()

let f x = Impl.f x (* 実際に公開したい関数 *)

実行時にCompiledNameが付いた型の元の名前を取得する

正確には実行時じゃないけど解決したのでメモ

元ネタ: http://ja.stackoverflow.com/questions/6177/compilednameが付いた型のf-での名前を取得したい

問題

F#では、CompiledNameAttributeを付けた型は、リフレクションで元の名前を取得できません。

[<CompiledName("Piyo")>]
type Hoge() = class end

というクラスを定義し、リフレクションで型名を取得しようとすると、

> typeof<Hoge>.Name;;
val it : string = "Piyo"

"Piyo"という文字列が返ります。

ソースコード上では"Hoge"という名前でしかアクセスできないのに、取得できないのは何かと不便ですね?

コンパイル前の名前が格納されている場所

アセンブリに、リソースとしてFSharpSignatureData.{AssemblyName}という名前で埋め込まれています。

typeof<Piyo>.Assembly.GetManifestResourceStream("FSharpSignatureData.Library1")

というコードで取得できます。

詳しいフォーマットは分かりませんが、取得できたバイナリを覗いてみると確かに"Hoge"という文字列が入っています。

FSharp.Compiler.Service を使うといい

これらのページを参考にすると、

http://fsharp.github.io/FSharp.Compiler.Service/ja/project.html https://github.com/fsharp/FSharp.Compiler.Service/issues/187

こんな感じのコードで取得できるみたいです。

module CompilerServiceStudy

[<CompiledName("Piyo")>]
type Hoge() = class end

open System.IO

let base1 = Path.GetTempFileName()
let fileName1 = Path.ChangeExtension(base1, ".fs")
let projFileName = Path.ChangeExtension(base1, ".fsproj")
let dllName = Path.ChangeExtension(base1, ".dll")

open Microsoft.FSharp.Compiler.SourceCodeServices

let asm = typeof<Hoge>.Assembly

let checker = FSharpChecker.Create()
let projectOptions =
  let sysLib name =
    System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86)
    + @"\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\"
    + (name + ".dll")
  let fsCore4300() =
    System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86)
    + @"\Reference Assemblies\Microsoft\FSharp\.NETFramework\v4.0\4.3.0.0\FSharp.Core.dll"  
  checker.GetProjectOptionsFromCommandLineArgs
    (projFileName,
      [| yield "--simpleresolution" 
         yield "--noframework" 
         yield "--debug:full" 
         yield "--define:DEBUG" 
         yield "--optimize-" 
         yield "--out:" + dllName
         yield "--warn:3" 
         yield "--fullpaths" 
         yield "--flaterrors" 
         yield "--target:library" 
         yield fileName1
         let references =
           [ sysLib "mscorlib" 
             sysLib "System"
             sysLib "System.Core"
             fsCore4300()
             asm.Location ]
         for r in references do 
               yield "-r:" + r |])
let wholeProjectResults = checker.ParseAndCheckProject(projectOptions) |> Async.RunSynchronously
let refAssemblies = wholeProjectResults.ProjectContext.GetReferencedAssemblies()

let thisAssembly = refAssemblies |> List.find (fun a -> a.SimpleName = asm.GetName().Name)
let thisModule = thisAssembly.Contents.Entities |> Seq.find (fun e -> e.DisplayName = "CompilerServiceStudy")
let piyoClass = thisModule.NestedEntities |> Seq.find (fun e -> e.CompiledName = "Piyo")
let hogeName = piyoClass.DisplayName
> CompilerServiceStudy.hogeName;;
val it : string = "Hoge"

無事"Hoge"が取得できました。ただし内部でコンパイルしているため、初めの1回はすごく遅いです。