LoginSignup
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-03

初めに

以前、関数型プログラミングの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

  • フロントエンド記述用の言語であるElmで採用されたパターン
  • MVU(Model-View-Update)が基本的な考え方
  • 有名なReduxなどのライブラリに影響を与えた

F#

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

Avalonia FuncUI

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

F#の基本的な文法

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

変数と関数

// 値の束縛
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")

型とパターンマッチ

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

以下のコードはチュートリアルを参考にしています。

Counter

カウンター

Model

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

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

Update&Message

//判別共用体で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

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

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

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#カウンター

その他の用意されているコンポーネント

  • テキストボックス
  • チェックボックス
  • ラジオボタン
  • アコーディオンメニュー
  • カレンダー

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

非同期処理

ElmishのCmdとF#のasync(task)コンピュテーション式で実現可能です。仕組み自体は、Elmとほとんど変わらないためElm公式リファレンスを読みましょう。

スタイリング

個人的にはinlineの方が楽ですが、パフォーマンスはどちらの方がいいんでしょうか(調査不足)

  • inline
  • XAML

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

Haskell

Monomer

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カウンター

Rust

Iced

#[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カウンター

最後に

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

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

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

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