私は普段仕事で ASP.NET Core と C# で開発しているのですが,先日この F# for C# programmers という動画を流していたら,何となく自分も C# を F# に書き換えたくなってきました。
元々,関数型言語には興味があり,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# のものとは変えてます。
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 という顔文字っぽい記号でやってやる必要がありました。
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) のようにアスタリスクで区切るのが分からず苦労しました。
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# での) タプルを返す時でも括弧なしでよいようです。関数 (といってもこの場合はメソッドか) の定義で括弧を使わなくていいのは関数型っぽいですね( ̄ー ̄)。
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 の設定値を取っています。
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 したサービスクラスを受け取ることができます。
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 初投稿でした。