LoginSignup
3

More than 3 years have passed since last update.

ASP.NET Core の C# を F# に書き換える

Last updated at Posted at 2019-10-27

私は普段仕事で ASP.NET Core と C# で開発しているのですが,先日この F# for C# programmers という動画を流していたら,何となく自分も C# を F# に書き換えたくなってきました:sunglasses:
元々,関数型言語には興味があり,Haskell などを勉強していたこともあるのですが,本格的に使うには至らず (尤もその考え方は OO 言語での開発にも役立っていると思います)。F# は正直全然使ったことがなく (当然周りも誰も使ってない),何となく敷居の高さを感じていましたが,上記の動画を見て,意外とできそうと錯覚?したので,まずは既存の API を F# で書き直してみようと思い,やってみました。具体的には,

  • Web API (ビューなし)
  • Entity Framework Core を使って SQLite にアクセス
  • ASP.NET Core にデフォルトで導入されている DI を使用

なお Visual Studio 2017, .NET Core 2.2 を使用しています。

最後にちょっと疑問?に思ったことも書きます。

F# のプロジェクトを作成

Visual Studio であれば,ASP.NET Core の WebAPI プロジェクトをテンプレートから作れます。コマンドであれば,dotnet new webapi -lang F# でしょうか。

モデルクラス及び DataContext を作成

ここからは既存の C# プロジェクトをベースに作っていきました。
Models フォルダを作成してモデルクラスを作成。複合キーテーブル用です。なおクラス名やフィールド名は実際の (書き換え対象である) C# のものとは変えてます。

Models/TestModel.cs
namespace FsharpTest.Models

open System

type [<CLIMutable>] TestModel =
 {
    Key1: int
    Key2: int
    Value1: decimal
    Value2: decimal
 }

データコンテキストクラスの作成。C# とほぼ同じですが簡単な文法に意外と苦戦しました。変更可能な変数は mutable を付けるとか,戻り値を棄てる場合は ignore というものがないといけないとか。複合キーの設定も,C# だと new {c.key1, c.key2} と匿名クラスオブジェクトを作る感じでしたが,F# だと :> Object という顔文字っぽい記号でやってやる必要がありました:slight_smile:

Data/FsharpDataContext.fs
namespace FsharpTest.Data

open Microsoft.EntityFrameworkCore
open FsharpTest.Models
open System

type FsharpDataContext(options: DbContextOptions<FsharpDataContext>) =
    inherit DbContext(options)

    [<DefaultValue>]
    val mutable _TestModel: DbSet<TestModel>
    member x.TestModel
        with get() = x._TestModel
        and set v = x._TestModel <- v
    override x.OnModelCreating(builder : ModelBuilder) =
        base.OnModelCreating(builder)
        builder.Entity<TestModel>().HasKey(fun c -> ( c.Key1, c.Key2 ) :> Object) |> ignore

そういえば,さらに「リポジトリ」を作る流儀もあるようなのですがここでは作りません。

サービスクラスのインタフェースの作成

ASP.NET Core になってから初めて DI を使ったのですが,「依存関係逆転の原則」のありがたみを実感しています。
F# でも当然可能なのでインタフェースを定義。インタフェースですけどメンバーを abstract キーワードを使って定義するのがちょっと戸惑いました。GetTaple ではタプルを返していますが,(double * double) のようにアスタリスクで区切るのが分からず苦労しました。

Services/Abstract/ITestService.cs
namespace FsharpTest.Service.Abstract

type ITestService =
    abstract member GetDouble : key1:int -> key2:int -> double
    abstract member GetTaple : key1:int -> key2:int -> (double * double)
    abstract member GetDbData : key1:int -> key2:int -> TestModel

サービスクラスの実装

インタフェースを実装します。ここでの FsharpDataContext ものちほど設定する DI でランタイムより渡されます。実装する側で interface キーワードを指定するというのも C# に慣れていると戸惑うところ。
本当の C# の実装はもっと面倒なことをしていますが,ここでは適当な値を返します。(C# での) タプルを返す時でも括弧なしでよいようです。関数 (といってもこの場合はメソッドか) の定義で括弧を使わなくていいのは関数型っぽいですね( ̄ー ̄)。

Services/TestService.cs
namespace FsharpTest.Service

open FsharpTest.Service.Abstract
open FsharpTest.Data

type TestService(context: FsharpDataContext) =
    interface ITestService with
        member this.GetDouble key1 key2 = 0.1
        member this.GetTaple key1 key2 = 0.1, 0.2
        member this.GetDbData key1 key2 = this.context.TestModel.First()

    member this.context with get() = context

データコンテキストとサービスクラスの DI

Startup.fs を編集し,データコンテキストとサービスクラスを Inject します (「追加↓」~「追加↑」以外の部分は namespace を読み込む open 以外手を付けていないので省略)。前述の通り DB は SQLite に Entity Framework Core で接続します。
実は,C# で使えていた GetConnectionString メソッドだと何故か値が取れず…。F# だからではない別の原因の可能性も高いですが,ひとまず GetSection メソッドで appsettings.json の設定値を取っています。

Startup.fs
type Startup private () =
    new (configuration: IConfiguration) as this =
        Startup() then
        this._configuration <- configuration

    // This method gets called by the runtime. Use this method to add services to the container.
    member this.ConfigureServices(services: IServiceCollection) =
        // 追加↓
        services.AddDbContext<FsharpDataContext>(
            fun options -> (options.UseSqlite(this._configuration.GetSection("ConnectionString:SQLiteConnection").Value) |> ignore) )
            |> ignore
        services.AddTransient<ITestService,TestService>() |> ignore
        // 追加↑
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2) |> ignore

    // (中略)

    member val _configuration : IConfiguration = null with get, set

コントローラで DI されたサービスクラスを受け取る

紹介する順番がこれで妥当なのか自信がありませんが(汗)最後にコントローラです。
これも C# と同じく DI したサービスクラスを受け取ることができます。

Controllers/TestController.fs
namespace FsharpTest.Controllers

open Microsoft.AspNetCore.Mvc
open FsharpTest.Service.Abstract

[<Route("api/[controller]")>]
[<ApiController>]
type TestController (service: ITestService) =
    inherit ControllerBase()
        member this.service with get() = service

    [<HttpGet>]
    member this.Get(key1:int, key2:int) =
        let value = service.GetDouble key1 key2
        ActionResult<double> value

以上でビルドして普通に C# と同じ動作をしました (SQLite ファイルは C# のを使いまわしているので新規作成していません)。

終わりに

ということで C# を F# に書き換えることはできました。感触としては,Web API であれば今まで C# で書いていたものを今後ほぼ全て F# で書くことも可能なのかなという気がしています。ビュー (Razor) についてはよく分かっていませんが。

ただ…DI 関連のことを書いているくらいから気づいていたのですが,これって F# である必要全然なくない?という気持ちにもなりました。結局,定義しているのはクラスであり,インスタンスが生まれているのであり,「関数」ではなく「メソッド」なのです。あまり関数型らしいことをしてないのでは,と。第一 DI 自体が,オブジェクト指向のポリモルフィズムを利用したものですしね。
勿論善悪の問題でもないし,これができるのも F# の長所なのでしょうけど。

とはいえ,自分の中の F# に対する敷居は確実に低くなったので,ASP.NET Core の C# を F# に書き換えてみるのはオススメです!

今後は,MSDN Magazine (2019/9) でも紹介されていた Giraffe などを試してみるのと,F# の文法から少し勉強して関数型らしいコーディングもできたらと思います。

Qiita 初投稿でした:v:

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3