FAKE4からFAKE5への移行手順
6月6日にFAKE5がリリースされました。
@fsharpMake is reborn with 5.0.0! Probably the biggest FAKE release ever, just look at the release notes (spoiler: everything is new): https://t.co/8xudlRxYN1
— FAKE - F# Make (@fsharpMake) June 6, 2018
この記事は、手元のプロジェクトをFAKE4からFAKE5に移行した時の記録です。 FAKE4ユーザー視点の内容が多いです。
FAKE5のドキュメント
英語しか無いですが、公式ドキュメントのチュートリアルを読めば大体のことは書いてあります。
FAKE4を使っている人は、マイグレーションガイドも読みましょう。
FAKE5の変更点
まずはリリースノートを読んでみます。 FAKE4を使っている人にとって特に重要なのがこれらの更新です。
要約すると、「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でビルドしないのなら必要ないです。
build.fsxとpaket.depencenciesを削除する
bootstrapをインストールするときにこの2つがあると実行できません。 あとでgitを使って復元します。
dotnet new fake --bootstrap project
を実行するbuild.fsx
、build.proj
、fake.cmd
、fake.sh
、paket.dependencies
が新しく作られます。build.fsxとpaket.depencenciesを復元する
git checout -- build.fsx paket.dependencies
これで、リポジトリのclone後にfake.cmd
を実行すると、FAKEがインストールされるようになります。
bootstrapの原理はfake.cmd
とbuild.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に移行する
先程洗い出した変更点の警告に従って、
paket.dependencies
にパッケージを追加intellisense.fsx
を再生成build.fsx
を編集
を繰り返していきます。
旧APIの移行先のモジュール名とパッケージ名は一致しています。 "Use Fake.DotNet.MSBuild instead"という警告が出た場合は、"Fake.DotNet.MSBuild"パッケージを追加して、"Fake.DotNet"名前空間をopenすると新APIを利用できます。
動作確認
動作確認して無事動いたら終了です。
自分の場合は、FAKE.PersimmonがFAKE5に対応していないので、同等のモジュールを自作することになりました。 他は特に問題なく動作しました。
ProjectScaffoldの状況
現在ProjectScaffoldはFAKE5に移行中です。急いでFAKE5に移行する必要もないので、ProjectScaffoldを使っている人は待ってみるのも良い選択だと思います。
Equality 制約
の実装のためのメモ(宣伝)。
型制約は 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())
A
はIEquatable
を実装しないため、Equality制約を満たさないと予想するかもしれない。
実際には前述のとおり、Equality制約を満たす。
Equality制約を違反する型を定義するには、NoEqualityAttirbute
を型に付ける。
[<NoEquality; NoComparison>] type B() = class end do test(B()) // 型 'B' は 'NoEquality' 属性があるため、'equality' 制約をサポートしません
NoEqualityAttribute
、CustomEqualityAttribute
はComparison制約系の属性NoComparisonAttribute
、CustomComparisonAttribute
と組み合わせて使用する。
CustomEqualityAttribute
を指定した場合はObject.Equals(obj)
、IEquatable<_>
、IStructualEquatable
のどれかを実装しなければならない。
Equality制約を満たす条件は次の通り。1、2は言語仕様書に定義されている。3、4は実装を読んだ。
NoEqualityAttribute
を持っていない型- 依存する型が全てEquality制約を満たす型
- レコード、判別共用体、構造体の型パラメータ、フィールドに使用される型すべて(クラスは対象外)
System.Tuple
の要素の型- ジェネリック型の
EqualityConditionalOnAttribute
が指定されたパラメータ
- 関数
FSharpFunc
ではない型(つまりFunc
、Action
はEquality制約を満たす) 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 XmlのSystem.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つの方法があります。
- シグネチャファイルを使う
- 非公開要素毎に
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回はすごく遅いです。
F# Puzzle 2
問題
次のコードは何が出力されるでしょうか?
let toUpper (x: string) -> x.ToUpper() if true then "hoge" else "piyo" |> toUpper続きを読む