Logicool G ゲーミングヘッドセット G335 ゲーミング ヘッドセット G335BK 超軽量 222g 3.5mm 有線 立体音響 ステレオ 2.1ch フリップミュート マイク付き PS5 PS4 PC Switch Xbox スマホ 対応 ヘッドホン ヘッドフォン ブラック 国内正規品
¥8,061 (2025年5月5日 13:12 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)KINGONEタッチペン【全機種対応 超高速充電】 2025業界最高 スタイラスペン 超高精度 1MM極細 アップルペンシル互換ペン 誤ON/OFF防止/電量表示/傾き検知/二重磁気吸着機能対応 iPad用ペンシル 軽量 耐摩 耐久 iPad/iPhone/Android/スマホ/タブレット対応タッチペン (ホワイト)
¥1,798 (2025年5月5日 13:17 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)
Rabbit-TEA は Moonbit で動く Elm モデルの宣言的 UI ライブラリ。
TEA は The Ele Architecture で、要は React+Redux みたいなやつ。
このテンプレートで vite + Tailwind 上で動かせる。
今の Moonbit は自分が集中的に触った 1 年前と比べて非同期や例外処理などと比べて、色々な機能が増えている。それを、このライブラリを使うことで確認していく。
簡単なカウンターの例
fnalias @html.(div, text, button)
enum Message {
Increment
}
struct Model {
count : Int
}
fn update(msg : Message, model : Model) -> (@tea.Command[Message], Model) {
match msg {
Increment => (@tea.none(), { count: model.count + 1 })
}
}
fn view(model : Model) -> @html.Html[Message] {
div(class="flex", [
button(class="p-4 bg-pink-200 rounded-lg select-none text-center", click=Increment, [
text("\{model.count}"),
]),
])
}
///| NOTE: This program is only available in the js backend,
fn main {
let model = { count: 0 }
@tea.startup(model~, update~, view~)
}
Elm パターン, 要は Redux のネタ元なので、 enum で代数的データ型を定義して、 Model を更新する。
model~
はキーワード引数 model=model
の省略形。
div や button の VDOM 構築は @html.div()
のような関数になっている。Moonbit は Rust 風の構文だが、キーワード引数があるので、props 相当の部分がここで補完可能なインターフェースが実現できている。
余談だが、 Rust Wasm の同等のライブラリの dioxus や yew は、可変長な同等の構文を proc-macro で実装しているが、補完までは実現できておらずあんまり使い勝手がよくない。
button() の型定義を見てみる
ライブラリの実態である .mooncakes/Yoorkin/rabbit-tea/src/html/html.mbt
で定義を見る。
pub fn button[M](
style~ : Array[String] = [],
id? : String,
class? : String,
click? : M,
childrens : Array[Html[M]]
) -> Html[M] {
let attrs = []
if click is Some(click) {
attrs.push(on_click(fn(_) { click }))
}
common_node(style~, class?, id?, "button", attrs, childrens)
}
現状、 HTML の attributes が全部定義できているわけではない。
このままだと disabled
や type=button
が書けない。
じゃあどうしたらいいか?とドキュメント読んでると、 node 属性を自分で定義すればいいらしい。
## Advanced Usage
The wrapper functions and properties provided here may not cover all possible use cases. If you encounter missing functionality, feel free to file an issue or use the `node()` function as a workaround. The `node()` function allows you to manually specify the tag name, attributes, and children for your HTML element, offering flexibility for advanced or uncommon scenarios.
```mbt
let html = node(
"div",
[style("key","value"), property("id","key")],
[child1, child2],
)
```
これはドキュメント的に少し古くて、 value 部分は @variant.Variant
型を取るらしい。
pub(all) enum Variant {
Boolean(Bool)
Integer(Int)
Floating(Double)
String(String)
} derive(Show, Eq, Compare, Hash)
disabled は確かにこれでしか表現できないので、納得感ある。
variant はそのまま export されてないので、 src/main/moon.pkg.json
を追記
{
"is-main": true,
"import": [
{
"path": "Yoorkin/rabbit-tea",
"alias": "tea"
},
{
"path": "Yoorkin/rabbit-tea/variant",
"alias": "variant"
},
"Yoorkin/rabbit-tea/html"
]
}
最終的にこうなった。
fn view(model : Model) -> @html.Html[Message] {
div(class="flex", [
button(
class="p-4 bg-pink-200 rounded-lg select-none text-center",
click=Increment,
[text("\{model.count}")],
),
@html.node(
"button",
[
@html.property("disabled", @variant.Boolean(true)),
@html.property("type", @variant.String("button")),
],
[text("Disabled")],
),
])
}
IDE のコードジャンプで書けるのがすごい。
非同期処理
.mooncakes/Yoorkin/rabbit-tea/src/example/async/main/main.mbt
fnalias @html.(div, text, button)
pub async fn suspend[T](f : ((T) -> Unit) -> Unit) -> T = "%async.suspend"
extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)"
enum Message {
Increment
Display(String)
LazyLog
}
struct Model {
count : Int
message : String
}
fn update(msg : Message, model : Model) -> (@tea.Command[Message], Model) {
match msg {
Increment =>
(@tea.none(), { count: model.count + 1, message: model.message })
Display(text) => (@tea.none(), { count: model.count, message: text })
LazyLog => {
let f = async fn() {
suspend!(fn(resolve) { set_timeout(fn() { resolve(()) }, 200) })
"Resolved"
}
println("LazyLog")
(@tea.perform(Message::Display, f), model)
}
}
}
fn view(model : Model) -> @html.Html[Message] {
div([
button(class="p-4 bg-pink-200", click=Increment, [text("\{model.count}")]),
@html.hr(),
@html.div(class="p-4 bg-amber-50", [
@html.p([text(model.message)]),
button(class="p-4", click=LazyLog, [text("show text after 5s")]),
]),
@html.hr(),
@html.node(
"button",
[
@html.property("disabled", @variant.Boolean(true)),
@html.property("type", @variant.String("button")),
],
[text("Disabled")],
),
])
}
fn main {
let model = { count: 0, message: "" }
@tea.startup(model~, update~, view~)
}
extern "js" fn set_timeout
で setTimeount の FFI を定義して、それを suspend!()
で呼び出している。
extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)"
LazyLog => {
let f = async fn() {
suspend!(fn(resolve) { set_timeout(fn() { resolve(()) }, 200) })
"Resolved"
}
println("LazyLog")
(@tea.perform(Message::Display, f), model)
}
@tea.startup() は何してる?
pub fn startup[Model, Message](
model~ : Model,
update~ : (Message, Model) -> (Cmd[Message], Model),
view~ : (Model) -> @html.Html[Message],
mount~ : String = "app"
) -> Unit {
@dom.document()
.get_element_by_id(mount)
.get_exn()
.set_inner_html("")
let mut sandbox = None
let mut curr_dom = @vdom.node("div", [], [])
fn after_update(html : @html.Html[Message]) {
guard sandbox is Some(sandbox)
let new_dom = html.to_virtual_dom()
new_dom.patch(curr_dom, sandbox, mount~)
curr_dom = new_dom
}
sandbox = Some(@browser.Sandbox::new(model, update, view, after_update~))
sandbox.unwrap().refersh()
}
requestAnimationFrame でメインループを回す
TODO: あとで書く。
途中まで書いたけど、イベントを介さずコマンドを dispatch する方法がわからない。
感想
とにかく IDE 補助が小気味よく、するする書ける。
とはいえライブラリの import はドキュメント足りない。
TS で書きづらい複雑なドメインロジックは、これで書いてみてもいいかも。
Views: 2