The Elm Architecture（F#+Avalonia FuncUI）によるGUIプログラミング

# 初めに
以前、関数型プログラミングのLT会で発表したものになります。
https://slidev-gui-by-elm-architecture.netlify.app/1

ElmのThe Elm Architectureは、非常にわかりやすい仕組みでUI構築できるデザインパターンです。しかし、ElmはWebアプリケーション用の言語で、デスクトップアプリケーションで利用する手段は考えられていません。Elm+Electoronでデスクトップアプリケーションを実装できそうですが、Elmでは直接ローカルのファイルを操作できず、Electronを介す必要があり面倒なことになりそうです。そのため、Elmに近い文法でデスクトップアプリケーションを実装できる言語とライブラリを探してみました。

# 今回紹介する技術
## [The Elm Architecture](https://guide.elm-lang.jp/architecture/)
- フロントエンド記述用の言語である**Elm**で採用されたパターン
- **MVU（Model-View-Update）**が基本的な考え方
- 有名な**Redux**などのライブラリに影響を与えた

## F# ##
- OCamlベースのマルチパラダイム言語
- VB、C#と同じく.Net上で実装されている
  - C#との関係性はJavaに対するScalaに近い？（個人の感想）
  - VBやC#とライブラリを共有できる

## [Avalonia FuncUI](https://github.com/fsprojects/Avalonia.FuncUI)
- C#のGUIライブラリ`Avalonia UI`のラッパー
- クロスプラットフォーム対応（WIndows、Mac、Linux）
- Elmishを採用している
    - Elmに近い文法でUIを記述できるライブラリ
    - MVUと相性が良い
- 基本的にXAMLを使わなくても記述できる
    - スタイルの指定など一部では利用する

# F#の基本的な文法 #
チュートリアルのコード読む上で最低限必要な文法を紹介

## 変数と関数

```fsharp
// 値の束縛
let x = 100

// 変数
let mutable y = 100

// 代入
y <- y * 2
// y = 200

// 関数
let f x = x * 2
printfn $"{f x}"
// 出力:200

// ラムダ式
let lambda = fun x -> $"{x}+1000={x + 1000}"
printfn $"{lambda x}"
// 出力:100+1000=1100

// パイプライン演算子
[ 1; 2; 3 ]
|> List.map (fun x -> x * 2)
|> List.map (printfn "%d")
```

### 型とパターンマッチ

```fsharp
open System

// レコード
type Solid =
    { width: float
      height: float
      depth: float }

// 判別共用体
type Shape =
    | Circle of float
    | Rectangle of width: float * height: float
    | Rectangular of Solid

// パターンマッチング
let calc =
    function
    | Circle r -> Math.PI * r ** 2.0
    | Rectangle (w, h) -> w * h
    | Rectangular { width = w; height = h; depth = d } -> w * h * d
let rectangle = calc <| Rectangle(3.0, 4.0)
printfn $"{rectangle}"
// 出力:12
```

# Avalonia FuncUI
以下のコードは[チュートリアル](https://github.com/fsprojects/Avalonia.FuncUI/wiki/Full-Template)を参考にしています。

## Counter
![カウンター](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591669/b8aa4985-b862-b302-e9e4-59b0a19486eb.png)

## Model

```fsharp
//レコードでModelを定義
type State = { count : int }

//Modelの初期値
let init = { count = 0 }
```

## Update&Message

```fsharp
//判別共用体でMessageを定義
type Msg =
    | Increment
    | Decrement
    | Reset

//Messageを元にパターンマッチングで分岐してStateを更新
let update (msg: Msg) (state: State) : State =
    match msg with
    | Increment -> { state with count = state.count + 1 }
    | Decrement -> { state with count = state.count - 1 }
    | Reset -> init
```

## View

```fsharp
let view (state: State) (dispatch) =
    //ボタンやテキストを並べるためのドックパネルを生成
    DockPanel.create [
        //ドックパネルの子要素はDockPanel.childrenの引数に配列として渡す
        //配列の後ろにある要素のほうが優先度が高くため、より高い（低い）位置に設置される
        DockPanel.children [
            //リセットボタン
            Button.create [
                Button.dock Dock.Bottom
                Button.onClick (fun _ -> dispatch Reset)
                Button.content "reset"
            ]
            //マイナスボタン
            Button.create [
                Button.dock Dock.Bottom
                Button.onClick (fun _ -> dispatch Decrement)
                Button.content "-"
            ]
            //プラスボタン
            Button.create [
                Button.dock Dock.Bottom
                Button.onClick (fun _ -> dispatch Increment)
                Button.content "+"
            ]
            //値を表示するためのテキストブロック
            TextBlock.create [
                TextBlock.dock Dock.Top
                TextBlock.fontSize 48.0
                TextBlock.verticalAlignment VerticalAlignment.Center
                TextBlock.horizontalAlignment HorizontalAlignment.Center
                TextBlock.text (string state.count)
            ]
        ]
    ]
```

## TabControl
タブによる画面の切り替えも簡単に実装できます。

```fsharp
let view (state: State) (dispatch) =
    DockPanel.create [
        DockPanel.children [
            //タブコントロールのバーを生成
            TabControl.create [
                TabControl.tabStripPlacement Dock.Top
                //タブとして表示するアイテムを配列として渡す
                TabControl.viewItems [
                    TabItem.create [
                        TabItem.header "Counter Sample"
                        //カウンターのViewにStateとDispatchを渡す
                        TabItem.content (Counter.view state.counterState (CounterMsg >> dispatch))
                    ]
                    TabItem.create [
                        TabItem.header "About"
                        //概要のViewにStateとDispatchを渡す
                        TabItem.content (About.view state.aboutState (AboutMsg >> dispatch))
                    ]
                ]
            ]
        ]
    ]
```

![F#カウンター](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591669/7d758b5f-8d4c-7c20-50ea-a975c22123f5.png)

## その他の用意されているコンポーネント
- テキストボックス
- チェックボックス
- ラジオボタン
- アコーディオンメニュー
- カレンダー

ここにない要素でもAvalonia UIに存在していれば、自身でラッパーを生成することで利用できます。

## 非同期処理
Elmishの`Cmd`とF#の`async（task）コンピュテーション式`で実現可能です。仕組み自体は、Elmとほとんど変わらないため[Elm公式リファレンス](https://guide.elm-lang.jp/effects/)を読みましょう。

## スタイリング
個人的にはinlineの方が楽ですが、パフォーマンスはどちらの方がいいんでしょうか（調査不足）

- inline
- XAML


# F#以外における言語と類似ライブラリ
## Haskell
### Monomer

```haskell
newtype AppModel = AppModel {
  _clickCount :: Int
} deriving (Eq, Show)
data AppEvent
  = AppInit
  | AppIncrease
  deriving (Eq, Show)
makeLenses 'AppModel
buildUI
  :: WidgetEnv AppModel AppEvent
  -> AppModel
  -> WidgetNode AppModel AppEvent
buildUI wenv model = widgetTree where
  widgetTree = vstack [
      label "Hello world",
      spacer,
      hstack [
        label $ "Click count: " <> showt (model ^. clickCount),
        spacer,
        button "Increase count" AppIncrease
      ]
    ] `styleBasic` [padding 10]
```

![Haskellカウンター](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591669/30f87760-8cc3-c340-b999-8970b0dc5df0.png)


## Rust
### Iced

```rust
#[derive(Default)]
struct Counter {
    value: i32,
    increment_button: button::State,
    decrement_button: button::State,
}
#[derive(Debug, Clone, Copy)]
enum Message {
    IncrementPressed,
    DecrementPressed,
}
impl Sandbox for Counter {
    type Message = Message;
    fn new() -> Self {
        Self::default()
    }
    fn title(&self) -> String {
        String::from("Counter - Iced")
    }
    fn update(&mut self, message: Message) {
        match message {
            Message::IncrementPressed => {
                self.value += 1;
            }
            Message::DecrementPressed => {
                self.value -= 1;
            }
        }
    }
    fn view(&mut self) -> Element<Message> {
        Column::new()
            .padding(20)
            .align_items(Alignment::Center)
            .push(
                Button::new(&mut self.increment_button, Text::new("Increment"))
                    .on_press(Message::IncrementPressed),
            )
            .push(Text::new(self.value.to_string()).size(50))
            .push(
                Button::new(&mut self.decrement_button, Text::new("Decrement"))
                    .on_press(Message::DecrementPressed),
            )
            .into()
    }
}
```

![Rustカウンター](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591669/36ac497b-2364-6748-5960-5c36c8a66097.png)


# 最後に
Elmに影響受けたライブラリが、思ってよりも色々ありました。Elmが書ければF#やHaskellの基礎を身につけることで、デスクトップアプリケーションも作れそうです。

また、C#が中心になると思いますが、現在.Net6でMVUモデルによるGUI開発が行えるようになるそうです。.Netはクロスプラットフォーム化も進んでいますし、今後も注目していきたいです。

個人的には、The Elm ArchitectureやMVUパターンが好きなので、今後もっと広めて行きたいですね。他にもおすすめのライブラリがあれば是非コメントで教えていただけると幸いです。