Declare what our application should do in pure functions, which are then executed by an UI runtime
“vdom” reactive type layers over gtk
A pure functional language that compiles to JavaScript.
Primarily designed to make web application and components.
Like Haskell:
Types:
Logic (pure functions):
view
update
init
Runtime
Specific types written with a capital letter: Int
,
Bool
, String
, etc.
these lowercase identifiers are type variables standing for arbitrary or generic types.
{ok : Bool, title : String}
The Model (or State) contains the dynamic data of the application (program)
examples:
Typically a product or algebraic data type (ADT)
eg
or
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
type alias Model = Int
init : Model
init = 0
type Msg = Increment | Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment -> model + 1
Decrement -> model - 1
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
main =
Browser.sandbox { init = init, update = update, view = view }
https://elm-lang.org/examples/buttons
Add a reset button to our counter.
Shows how simple to refactor
sandbox
A “sandboxed” program that cannot communicate with the outside world.
sandbox :
{ init : model
, view : model -> Html msg
, update : msg -> model -> model
}
-> Program () model msg
https://package.elm-lang.org/packages/elm/browser/latest/Browser#sandbox
init ==> view =msg=> update ==> view =msg=> update ==> view =msg=> ...
-- https://guide.elm-lang.org/architecture/text_fields.html
import Browser
import Html exposing (Html, Attribute, div, input, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
type alias Model =
{ content : String
}
init : Model
init = { content = "" }
type Msg = Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Text to reverse", value model.content, onInput Change ] []
, div [] [ text (String.reverse model.content) ]
]
main = Browser.sandbox { init = init, update = update, view = view }
Show length using String.length
https://elm-lang.org/examples/forms
-- https://guide.elm-lang.org/architecture/forms.html
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
type alias Model =
{ name : String
, password : String
, passwordAgain : String
}
init : Model
init = Model "" "" ""
type Msg
= Name String
| Password String
| PasswordAgain String
update : Msg -> Model -> Model
update msg model =
case msg of
Name name ->
{ model | name = name }
Password password ->
{ model | password = password }
PasswordAgain password ->
{ model | passwordAgain = password }
view : Model -> Html Msg
view model =
div []
[ viewInput "text" "Name" model.name Name
, viewInput "password" "Password" model.password Password
, viewInput "password" "Re-enter Password" model.passwordAgain PasswordAgain
, viewValidation model
]
viewInput : String -> String -> String -> (String -> msg) -> Html msg
viewInput t p v toMsg =
input [ type_ t, placeholder p, value v, onInput toMsg ] []
viewValidation : Model -> Html msg
viewValidation model =
if model.password == model.passwordAgain then
div [ style "color" "green" ] [ text "OK" ]
else
div [ style "color" "red" ] [ text "Passwords do not match!" ]
main =
Browser.sandbox { init = init, update = update, view = view }
Check that password is at least 8 characters.
element
element :
{ init : flags -> ( model, Cmd msg )
, view : model -> Html msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
}
-> Program flags model msg
https://package.elm-lang.org/packages/elm/browser/latest/Browser#element
https://package.elm-lang.org/packages/elm/core/latest/Platform-Cmd#Cmd
https://guide.elm-lang.org/effects/random
import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Random
type alias Model =
{ dieFace : Int }
init : () -> (Model, Cmd Msg)
init _ =
( Model 1
, Cmd.none
)
type Msg
= Roll
| NewFace Int
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Roll ->
( model
, Random.generate NewFace (Random.int 1 6)
)
NewFace newFace ->
( Model newFace
, Cmd.none
)
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
view : Model -> Html Msg
view model =
div []
[ h1 [] [ text (String.fromInt model.dieFace) ]
, button [ onClick Roll ] [ text "Roll" ]
]
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init =Cmd=> view =msg=> update =Cmd=> view =msg=> update =Cmd=> view =msg=> ...
Yes
gtk2hs
haskell-gi
https://owickstrom.github.io/gi-gtk-declarative/
https://github.com/owickstrom/gi-gtk-declarative/blob/master/examples/Functor.hs
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Functor where
import Control.Monad ( void )
import Data.Functor ( ($>) )
import Data.Text ( Text )
import qualified Data.Text as Text
import GI.Gtk ( Box(..)
, Button(..)
, Label(..)
, Orientation(..)
, Window(..)
)
import GI.Gtk.Declarative
import GI.Gtk.Declarative.App.Simple
data ButtonEvent = ButtonClicked
clickyButton :: Text -> Widget ButtonEvent
clickyButton label = widget Button [#label := label, on #clicked ButtonClicked]
data State = State { count :: Integer }
data Event = Incr | Decr | Closed
view' :: State -> AppView Window Event
view' State {..} =
bin
Window
[ #title := "Functor"
, on #deleteEvent (const (True, Closed))
, #widthRequest := 400
, #heightRequest := 300
]
$ container
Box
[#orientation := OrientationVertical]
[ expandingChild $ widget Label [#label := Text.pack (show count)]
, BoxChild defaultBoxChildProperties $ container
Box
[#orientation := OrientationHorizontal]
[ expandingChild $ clickyButton "-1" $> Decr
, expandingChild $ clickyButton "+1" $> Incr
]
]
where
expandingChild =
BoxChild defaultBoxChildProperties { expand = True, fill = True }
update' :: State -> Event -> Transition State Event
update' State {..} Incr = Transition (State (count + 1)) (return Nothing)
update' State {..} Decr = Transition (State (count - 1)) (return Nothing)
update' _ Closed = Exit
main :: IO ()
main = void $ run App { view = view'
, update = update'
, inputs = []
, initialState = State 0
}
https://github.com/owickstrom/gi-gtk-declarative/blob/master/examples/Exit.hs
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module Exit where
import Control.Concurrent ( threadDelay )
import Control.Monad ( void )
import Data.Functor ( ($>) )
import qualified Data.Text as Text
import GI.Gtk ( Button(..)
, Label(..)
, Window(..)
)
import GI.Gtk.Declarative
import GI.Gtk.Declarative.App.Simple (App(..), AppView,
Transition(..), run)
data State = Running | ExitingIn Int
data Event = ExitApplication | CountDownExit
view' :: State -> AppView Window Event
view' s =
bin
Window
[ #title := "Exit"
, on #deleteEvent (const (True, ExitApplication))
, #widthRequest := 400
, #heightRequest := 300
]
$ case s of
Running ->
widget Button [#label := "Exit", on #clicked ExitApplication]
ExitingIn sec -> widget
Label
[#label := ("Exiting in " <> Text.pack (show sec) <> " seconds.")]
countDown :: IO (Maybe Event)
countDown = threadDelay oneSec $> Just CountDownExit
where
oneSec :: Int
oneSec = 1000000
update' :: State -> Event -> Transition State Event
update' Running ExitApplication = Transition (ExitingIn 3) countDown
update' Running _ = Transition Running (pure Nothing)
update' (ExitingIn 1) CountDownExit = Exit
update' (ExitingIn sec) CountDownExit = Transition (ExitingIn (pred sec)) countDown
update' s@ExitingIn{} ExitApplication = Transition s (pure Nothing)
main :: IO ()
main = void $ run App { view = view'
, update = update'
, inputs = []
, initialState = Running
}
https://github.com/owickstrom/gi-gtk-declarative/blob/master/examples/AddBoxes.hs
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module AddBoxes where
import Control.Monad ( void )
import qualified Data.Text as Text
import Data.Vector ( Vector )
import qualified Data.Vector as Vector
import GI.Gtk ( Box(..)
, Button(..)
, Label(..)
, Orientation(..)
, PolicyType(..)
, ScrolledWindow(..)
, Window(..)
)
import GI.Gtk.Declarative
import GI.Gtk.Declarative.App.Simple
data Event = AddLeft | AddRight | Closed
data State = State { lefts :: Vector Int, rights :: Vector Int, next :: Int }
addBoxesView :: State -> AppView Window Event
addBoxesView State {..} =
bin
Window
[ #title := "AddBoxes"
, on #deleteEvent (const (True, Closed))
, #widthRequest := 400
, #heightRequest := 300
]
$ bin
ScrolledWindow
[ #hscrollbarPolicy := PolicyTypeAutomatic
, #vscrollbarPolicy := PolicyTypeNever
]
$ container Box
[#orientation := OrientationVertical]
[renderLane AddLeft lefts, renderLane AddRight rights]
where
renderLane :: Event -> Vector Int -> BoxChild Event
renderLane onClick children =
BoxChild defaultBoxChildProperties { padding = 10 } $ container
Box
[]
( BoxChild defaultBoxChildProperties { padding = 10 } btn
`Vector.cons` Vector.map
( BoxChild defaultBoxChildProperties { padding = 5 }
. renderChild
)
children
)
where btn = widget Button [#label := "Add", on #clicked onClick]
renderChild :: Int -> Widget Event
renderChild n = widget Label [#label := Text.pack (show n)]
update' :: State -> Event -> Transition State Event
update' state@State {..} AddLeft = Transition
state { lefts = lefts `Vector.snoc` next, next = succ next }
(return Nothing)
update' state@State {..} AddRight = Transition
state { rights = rights `Vector.snoc` next, next = succ next }
(return Nothing)
update' _ Closed = Exit
main :: IO ()
main = void $ run App { view = addBoxesView
, update = update'
, inputs = []
, initialState = State [1] [2] 3
}
a video editor
https://github.com/owickstrom/komposition
https://wickstrom.tech/2018-10-26-writing-a-screencast-video-editor-in-haskell.html
https://github.com/Relm4/Relm4/blob/main/examples/simple_manual.rs
use gtk::glib::clone;
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt};
use relm4::{gtk, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt, SimpleComponent};
struct AppModel {
counter: u8,
}
#[derive(Debug)]
enum AppInput {
Increment,
Decrement,
}
struct AppWidgets {
label: gtk::Label,
}
impl SimpleComponent for AppModel {
/// The type of the messages that this component can receive.
type Input = AppInput;
/// The type of the messages that this component can send.
type Output = ();
/// The type of data with which this component will be initialized.
type Init = u8;
/// The root GTK widget that this component will create.
type Root = gtk::Window;
/// A data structure that contains the widgets that you will need to update.
type Widgets = AppWidgets;
fn init_root() -> Self::Root {
gtk::Window::builder()
.title("Simple app")
.default_width(300)
.default_height(100)
.build()
}
/// Initialize the UI and model.
fn init(
counter: Self::Init,
window: &Self::Root,
sender: ComponentSender<Self>,
) -> relm4::ComponentParts<Self> {
let model = AppModel { counter };
let vbox = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(5)
.build();
let inc_button = gtk::Button::with_label("Increment");
let dec_button = gtk::Button::with_label("Decrement");
let label = gtk::Label::new(Some(&format!("Counter: {}", model.counter)));
label.set_margin_all(5);
window.set_child(Some(&vbox));
vbox.set_margin_all(5);
vbox.append(&inc_button);
vbox.append(&dec_button);
vbox.append(&label);
inc_button.connect_clicked(clone!(@strong sender => move |_| {
sender.input(AppInput::Increment);
}));
dec_button.connect_clicked(clone!(@strong sender => move |_| {
sender.input(AppInput::Decrement);
}));
let widgets = AppWidgets { label };
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message {
AppInput::Increment => {
self.counter = self.counter.wrapping_add(1);
}
AppInput::Decrement => {
self.counter = self.counter.wrapping_sub(1);
}
}
}
/// Update the view to represent the updated model.
fn update_view(&self, widgets: &mut Self::Widgets, _sender: ComponentSender<Self>) {
widgets
.label
.set_label(&format!("Counter: {}", self.counter));
}
}
fn main() {
let app = RelmApp::new("relm4.test.simple_manual");
app.run::<AppModel>(0);
}
Hope I have convinced you that Declarative UIs are simpler and easier to maintain, debug, and scale.
Many other interesting reactive/declarative projects in development, but in this tutorial talk I tried to focus on the purest kinds.
Blog post: https://engineering.rakuten.today/post/elm-at-rakuten/
Checkout also:
Like Haskell, even if you don’t continue Declarative UI programming, learning it will still make you a better application programmer.