paket.referenceで複数のバージョン依存を設定する方法

.NET Core SDKというかnew fsprojになってから、以下のような依存関係を書く機会によく遭遇する。

<ItemGroup Condition="'$(TargetFramework)'=='net45'">
  <PackageReference Include="FSharp.Core" Version="3.1.2.5" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='netstandard1.6'">
  <PackageReference Include="FSharp.Core" Version="4.1.18" />
</ItemGroup>

古い環境ではなるべく最低ラインのバージョンを指定しつつ、netstandardでは比較的新しめのバージョンを指定するというもの。 ただこの書き方は現時点のVS2017だとちゃんと認識されないので困った。

というわけでこれと同等の書き方をPaketで再現させる。

まずはpaket.dependenciesを用意。

framework: netstandard1.6
source https://api.nuget.org/v3/index.json

nuget FSharp.Core >= 4.1.18 lowest_matching:true

group Legacy
  framework: net45
  source https://api.nuget.org/v3/index.json

  nuget FSharp.Core >= 3.1.2.5 lowest_matching:true

framework targetごとにグループをわけておく。 これで異なるバージョンの、最も小さいバージョンが指定できる。

paket.reference側では次のように記述する。

FSharp.Core
group Legacy
  FSharp.Core

二つのグループについて依存関係を追加している。 これで、frameworkごとに異なるバージョンを指定できる。 この方法だとVS2017も正しく依存関係を認識する。

ちなみにpaket.templateでは、以下のようにLOCKEDVERSION-グループ名で記述する。

dependencies
  framework: net45
    FSharp.Core >= LOCKEDVERSION-Legacy
  framework: netstandard1.6
    FSharp.Core >= LOCKEDVERSION

某講座のミニゲームを F# で実装してみよう

https://dwango.github.io/articles/shachiku-chan-vol3/この記事関連で以下の反応をみかけた。

じゃあ試しにやってみましょうか、F# (というかFable)で。

https://github.com/pocketberserker/SCV3

実質の実装箇所は以下。

https://github.com/pocketberserker/SCV3/blob/11dc1bece095314b4118e5350530f635576bedb2/src/App.fs

  • 面倒だったので全部floatで操作
  • loopの第2引数は使わないけど定義しておかないとBrowser.FrameRequestCallbackと型があわなくて怒られる
  • U3.Case1の初見殺し感よ
  • addEventListner系に渡す関数は雑に作りたかったのでnullを返しておく(つらい…)
  • 本当はBrowser.document.getElementById("canvas")canvasを取得したかったが、なぜか取得できなかったのでBrowser.document.getElementsByTagName_canvas().[0]お茶を濁す(そのうち調べます…)
  • 元コードが全力副作用なので素直に移植したらそりゃ代入祭りになるよね、という気持ち

F# で `a == 1 && a == 2 && a == 3` と `2 + 2 == 5`

もう誰かやってるかもしれない。 記事にネタがなくて困っていたので許して。

a == 1 && a == 2 && a == 3

https://stackoverflow.com/questions/48270127/can-a-1-a-2-a-3-ever-evaluate-to-true

let (&&) _ _ = true

警告されるが

warning FS0086: The '&&' operator should not normally be redefined. Consider using a different operator name

実行可能(以下>;;がついた部分はfsharp interactive)

> let a = 1;;            
val a : int = 1

> a = 1 && a = 2 && a = 3;;
val it : bool = true

2 + 2 == 5

加算は警告なし。

let (+) _ _ = 5
> 2 + 2 = 5;;
val it : bool = true

=だと

let (=) _ _ = 5

警告あり

warning FS0086: The '=' operator should not normally be redefined. To define equality semantics for a type, override the 'Object.Equals' member in the definition of that type.

番外編

let (+) _ _ = "ミソスープ"
1 + 1 |> printfn "%A"

あるいは、パイプライン演算子をミソスープ出力器にする。

let (|>) _ _ = printfn "ミソスープ"
1 + 1 |> printfn "%A"

???「ところで、printfnをミソスープにしてもかまわんのだろう?」

let printfn _ _ = printfn "ミソスープ"
1 + 1 |> printfn "%A"

???「ミソスープだけを作る機械かよ!?」

golangのcobraで、サブコマンドとして外部コマンドを実行する

新年が明けたのでgolangに入門し、お試しにとコマンドラインツールを作っているところです。

その際に、外部コマンドをサブコマンドのように実行するにはどうすれば…と思って見つけたのが以下の記事です。

[Go言語]codegangsta/cliで、サブコマンドとして外部コマンドをとれるようにしてみた - Qiita

urfave/cli*1を使えばまさにやりたいことができそう…と思っていたのですが、上記記事のコードでは引数に未知のflagが含まれているとエラーになってしまいます。

どうにか解決できないか、あるいは自分の勘違いなのではと調査したものの進展なし。 早々に諦めて別のライブラリを調査することに。

https://github.com/spf13/cobra

結果、わりと知名度が高そうなcobraで同様のことができたので掲載しておきます。 importやコマンド名は適宜置き換えてください。

// main.go
package main

import "sample/cmd"

func main() {
    cmd.Execute()
}

main.goは特筆すべきことなどありません。

// cmd/root.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "os"
    "os/exec"
)

var rootCmd = &cobra.Command{
    Use:  "sample",
    Run: func(cmd *cobra.Command, args []string) {},
}

func Execute() {

    args := os.Args
    if len(args) > 1 {
        trySearchSubCommand(args)
    }

    err := rootCmd.Execute()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

func trySearchSubCommand(args []string) {
    subcommand := args[1]

    for _, c := range rootCmd.Commands() {
        if c.Use == subcommand {
            return
        }
    }

    path, err := exec.LookPath(rootCmd.Use + "-" + subcommand)
    if err != nil {
        return
    }

    sub := &cobra.Command{
        Use:                subcommand,
        DisableFlagParsing: true,
        RunE: func(cmd *cobra.Command, args []string) error {
            c := exec.Command(path, args...)
            c.Stdout = os.Stdout
            c.Stdin = os.Stdin
            c.Stderr = os.Stderr
            return c.Run()
        },
    }
    rootCmd.AddCommand(sub)
}

func init() {}
  • rootCmd.Execute実行よりも前に探索を行う
  • rootCmd.Commandsで既存サブコマンド一覧を取得し、名前が一致するなら何もしない
  • exec.LookPathを使ってPATHの通った実行可能ファイルの絶対パスを探す
  • 見つかったならrootCmd.AddCommandでサブコマンドとして登録
    • DisableFlagParsing: trueにしてflagの解析を行わないようにする
  • 見つからない場合はrootコマンドの可能性が高いので何もしない

かなり無理矢理感がある、これで本当に良いのでしょうかね…golangcliライブラリに詳しい方にご教授いただきたいところ。

追記

今回の実装はrootコマンドのflagはparseしてほしい、かつ外部コマンドをサブコマンドとして実装したい場合の方法です。 rootにflagがない場合は下記で指摘されている通り、rootコマンドのRunで解析したほうが楽です。

objectxplosiveさん、指摘ありがとうございました。

…と、ここまでかいたタイミングで「rootの実態すら別コマンドとして実装して、root.Runではそいつを呼び出せばよかったのでは?」と思ったわけですが、試したわけではないので動くかわかりません。

*1:いつの日かcodegangsta/cliからrenameされたらしい

F# を使って雑にExcelから取り出したデータをElasticsearchに放り込む

この記事はF# Advent Calendar 2017の25日目の記事です。 大遅刻しましたごめんなさい…。

とあるExcelファイルがでかすぎて閲覧するのに苦痛だったので書きなぐった時のメモ。 .NET Coreです。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="EPPlus" Version="4.5.0.1-beta" />
    <PackageReference Include="Nest" Version="5.6.0" />
  </ItemGroup>
</Project>

まだbeta段階ですがEPPlusもnetstandard版があります。

open System
open System.IO
open OfficeOpenXml
open Nest

let getSheet (sheet: string) (excel: ExcelPackage) =
  excel.Workbook.Worksheets.[sheet]

type Questionnaire = {
  Value: string
}

let getQuestionnaire (excel: ExcelPackage) =
  let sheet = getSheet "アンケート" excel
  seq {
    for i in [ 2 .. Seq.length sheet.Cells - 1] ->
      {
        Value = sheet.Cells.[i, 1].Text
      }
  }

let questionnairesIndex = IndexName.op_Implicit "questionnaires"

let createIndex (name: IndexName) (client: ElasticClient) =
  client.CreateIndex(name)
  |> ignore

let insertQuestionnaire (client: ElasticClient) (messages: Questionnaire seq) =
  client.IndexMany(messages, questionnairesIndex, TypeName.op_Implicit "questionnaire")
  |> ignore

[<EntryPoint>]
let main argv =

  let file = argv.[0]
  let url = argv.[1]
  let user = argv.[2]
  let pass = argv.[3]

  use excel = new ExcelPackage(File.OpenRead(file))

  printfn "try to connect %s" url

  use settings =
    (new ConnectionSettings(Uri(url)))
      .BasicAuthentication(user, pass)
      .DisableDirectStreaming()
  let client = ElasticClient(settings)

  createIndex questionnairesIndex client

  getQuestionnaire excel
  |> insertQuestionnaire client

  0

仕様だったりCellにheaderが付いていた関係だったりでindexが少々ずれていますが気にしないでください。 雑に取得してdockerで立ち上げたElasticsearchに放り込むくらいだったらこれで十分ですはい。

とはいえElasticsearch 6.0系からmulti typeが廃止されたという事実に気づかずどハマりしましたが…。

dotnet new用のテンプレートを使う、作る

この記事はF# Advent Calendar 2017の6日目の記事です。 F#固有の話ではないですが、知っていると少し捗るかもしれない話をします。

dotnet newコマンドでnugetに公開されたテンプレートを使う

dotnet newは.NET Core SDKでテンプレートを用いてプロジェクトを作成するコマンドです。

https://docs.microsoft.com/ja-jp/dotnet/core/tools/dotnet-new?tabs=netcore2x

リポジトリはこちら:

https://github.com/dotnet/templating

標準でいくつかのテンプレートが搭載されていますが、dotnet new installコマンドを使えばnugetに公開されているテンプレートをインストールできます。

テンプレートを探すには下記サイトが便利です(公式のwikiで紹介されていたサイト)。

dotnet templates

たとえば、私を含む数名(?)で開発しているPersimmonのテンプレートはこんな感じでヒットします。

http://dotnetnew.azurewebsites.net/template/Persimmon.Templates/Persimmon.FSharp

テンプレートを作る

テンプレートの構成や設定は公式のwikiをみるのが一番です。

https://github.com/dotnet/templating/wiki/%22Runnable-Project%22-Templates

プロジェクトファイルは言語にあわせて用意します。 F#の場合は.fsprojですね(と申し訳程度のF#要素を挟んでおく)。

設定のうちshortNameは実際にdotnet newコマンドで使うので名前を吟味したほうが良いでしょう。 あとは、groupIdentityが同一でtagslanguageが異なるテンプレートが登録されていると-langオプションで言語選択できるようになります。

あと、複数言語のテンプレートを用意したいプロジェクト向けとして、1つのnugetパッケージには複数のテンプレートをほうり込めます。 これに関しては具体例をみたほうが早いので、nunitリポジトリをとりあげておきます。

https://github.com/nunit/dotnet-new-nunit

テンプレートをnugetで公開する

いつものごとくnuspecを書くわけですが、テンプレート用のパッケージであることを明示するためにmetadataタグ下にpackageTypesを追加します。

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
  ...
  <packageTypes>
    <packageType name="Template" />
  </packageTypes>
  </metadata>
  <files>
    <file src="..\templates\**" target="" />
  </files>
</package>

ファイルはテンプレートを置いているディレクトリ下のコードを全部取り込んでも問題ないことがほとんどだと思います。

おわりに

CLIで完結するのでテンプレートを作るハードルがそこそこ低いです。 publicなものに限らず社内NuGetに放り込んで運用することも可能…なはずなので、覚えておいて損はないと思います。

F# で Entity Framework Coreる

この記事はF# Advent Calendar 2017の5日目の記事です。 遅刻しましたごめんなさい…。

F# + .NET Core + Entity Frameworkの組み合わせて開発してみたときの備忘です。 もしかしたら多少古い知識が混ざっているかもれません(全部再検証するには時間がたりなかった)。

モデル定義

CLIMutableにしてEntityFramework側で処理できるようにしておきます。  また、マイグレーション機能を使うためにいくつかAttributeをつけて起きます。

open System
open System.ComponentModel.DataAnnotations
open System.ComponentModel.DataAnnotations.Schema

type UserId = int64

[<CLIMutable>]
type User = {
  [<Key; DatabaseGenerated(DatabaseGeneratedOption.Identity)>]
  Id: UserId
  [<Required; StringLength(32)>]
  Name: string
}

DBContext

だいたいC#のサンプルを翻訳していくだけです。

open Microsoft.EntityFrameworkCore
open Microsoft.Extensions.PlatformAbstractions

type HogeDbContext() =
  inherit DbContext()

  [<DefaultValue>]
  val mutable user : DbSet<User>

  static member val ConnectionString = "" with get, set

  member this.Users
    with get() = this.user
    and set v = this.user <- v

  override __.OnConfiguring(optionsBuilder: DbContextOptionsBuilder) =
    optionsBuilder.UseMySql(SpellbookLibraryDbContext.ConnectionString)
|> ignore

ここではMySQLでの例をだしましたが、他のDBでも大した違いはないはずです。

マイグレーションコード

C#であれば Microsoft.EntityFrameworkCore.Tools.DotNet をインストールしてコマンドを叩いてあげれば C#マイグレーションコードが生成されます。

ではF#であればF#のコードをだして…くれません。

https://github.com/aspnet/EntityFrameworkCore/pull/10289

このpull requestと関連issueを読めばわかりますが、C#以外の言語はコミュニティに任せたそうな雰囲気を感じます。 とはいえ、このpull requestを含む2.1.0はまだリリースされていませんし、コミュニティでツールが作成されるのもそれなりに時間がかかるでしょう。

では、マイグレーションを諦めるのか…といえば、いやいや、方法はあります。

  1. マイグレーション部分だけC#でやる
  2. 生成されたC#コードをすべてF#に書き直す

後者に関しては、F#で書いたモデルクラスからもマイグレーションコードを生成できるので、マイグレーションコードさえなんとかしてしまえば良いわけです。

ただし、2テーブルの作成に200行ほどのF#を書かないといけないので労力に見合っていないのがネックです。

気合で書くか、待つか、ツールを実装するか。 お好みのものをお選び下さい。