初めに
以前、関数型プログラミングの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))
]
]
]
]
]
その他の用意されているコンポーネント
- テキストボックス
- チェックボックス
- ラジオボタン
- アコーディオンメニュー
- カレンダー
ここにない要素でも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]
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()
}
}
最後に
Elmに影響受けたライブラリが、思ってよりも色々ありました。Elmが書ければF#やHaskellの基礎を身につけることで、デスクトップアプリケーションも作れそうです。
また、C#が中心になると思いますが、現在.Net6でMVUモデルによるGUI開発が行えるようになるそうです。.Netはクロスプラットフォーム化も進んでいますし、今後も注目していきたいです。
個人的には、The Elm ArchitectureやMVUパターンが好きなので、今後もっと広めて行きたいですね。他にもおすすめのライブラリがあれば是非コメントで教えていただけると幸いです。