2019/08/12
Elm関西 発表資料
自己紹介
-
arowM
-
おしごと: 広義のUX設計
-
ヤギコンサルタント/ヤギアイドルさくらちゃんのプロデュース
- さくらちゃん特大ワッペンつくりました
-
気吹堂(出版) 代表
- さくらちゃん写真集
-
ARoW(IT技術) 代表
- 技術顧問
- 技術補佐
- 要件分析・UX設計
-
株式会社UZUZ(人材紹介) CXO (ユーザー体験の責任者)
- 組織制度の改善
- マネジメント
- 社員のモチベーションマネジメント
- 業務フローの改善 (業務アプリの作成を含む)
- 求職者向けアプリの設計・開発
- 提携企業向けアプリの設計・開発
-
OSS活動
- 公式ガイド Elm guide の日本語翻訳プロジェクト
- Form decoderの概念の提唱とElm版実装
- Haskell製型安全なテンプレートエンジンheterocephalus
- プラグイン形式でさくさくっとバックエンドを作れちゃうフレームワークtonatona
-
概要
Elmのアプリケーションフレームワーク The Elm Architecture (TEA) の入門として、すご〜くシンプルなゲームを作ってみましょう。
TEAはUX設計と親和性がめちゃんこ高いよ!
ただし、ちゃんとTEAを身につけるならElm guideを読みましょう!
シナリオ
「シナリオ」はUX設計をする上で最も大切なテクニックの1つです。
ユーザーの気持ちを妄想して、ユーザーの行動を未来予知して、事前に文章に列挙します。
「まずシナリオを書いてみましょう」と助言しても、「そんなの素人がやるやつじゃん」と、いきなり実装を初めてクソアプリを作る人たちがいっぱいでヤギさんは悲しいです。
いい製品とは、粘り強く根気強く試行錯誤を何度も重ねたもの
じゃあ、先に妄想の中で試行錯誤した方が短期間でいい製品ができるじゃん
では、実際に「さくらちゃん と じゃんけん」ゲームのシナリオを作っていきましょう!
シナリオ1 -- ヤギ思いのユーザー
- このスタイルの文: アプリ側の表示
- 地の文: ユーザーの思考・行動
- ヤギの写真とともに「じゃ〜んけ〜ん」と画面に表示され、「ぐー」「ちょき」「ぱー」のボタンが並んでいる
- 「じゃんけんゲームってことかな?」
- とりあえず「ぐー」を押してみた
- 「かち!」と表示され、ヤギが悔しがった顔をしながら「ヤギさんにも ようしゃが ないね!」と言ってくる
- 「そっか、ヤギさんだからチョキしか出せないのか。悪いことしちゃったな」
- 「もういっかい」のボタンがあることに気づく
- 「あ、もしかしてもう一回じゃんけんできる? 今度は負けてあげよう」
- 「もういっかい」のボタンをクリックする
- 最初の画面が再び表示される
- 「あ、やっぱりもういっかいできるみたいだ」
- 「ぱー」を押してみた
- 「まけ!」と表示され、ヤギが調子に乗った顔しながら「ほもさぴえんすって よわい いきものだね!」と言ってくる
- 「なんかうざいけど、でも喜んでくれてよかった。」
シナリオ2 -- 察しが悪いユーザー
UX設計は、「まさかそんなことするか?!」というところまで妄想するといいUXができる。
- ヤギの写真とともに「じゃ〜んけ〜ん」と画面に表示され、「ぐー」「ちょき」「ぱー」のボタンが並んでいる
- 「え、なによくわからん」
- とりあえずヤギの写真をクリックしてみた
- 荒ぶった感じのヤギが「やぎさんじゃなくて ぼたんをおしてね!」って言ってくる
- 「あ、ボタンか。なるほど」
- とりあえず「ぐー」を押してみた
- (以下略)
画面設計
シナリオをもとに、画面と状態を整理します。
基本画面構成
以下のエリアから構成されます。
- ヤギ: ヤギの写真が表示される
- メインメッセージ: ユーザーへのメインのメッセージを表示する
- サブメッセージ: ユーザーへの補助的なメッセージを表示する
- ボタンエリア: ボタンを表示する
初期状態
- [画面]
- ヤギ: ノーマルやぎ
- メインメッセージ: 「じゃ〜んけ〜ん」
- サブメッセージ: 「すきな ぼたんを おしてね!」
- ボタンエリア:
- 「ぐー」
- 「ちょき」
- 「ぱー」
- [ユーザー] 「なにこれ、どれかボタン押せばいいの?」
- [イベント1] ぐーを押した
- [イベント2] ちょきを押した
- [イベント3] ぱーを押した
- [イベント4] ヤギを押した
イベント1
- [画面]
- ヤギ: 悔しそうなやぎ
- メインメッセージ: 「かち!」
- サブメッセージ: 「ヤギさんにも ようしゃが ないね!」
- ボタンエリア
- 「もういっかい」
- [ユーザー]
- [イベント5] 「もういっかい」を押した
イベント2
- [画面]
- ヤギ: 絶妙な表情のヤギ
- メインメッセージ: 「あいこ!」
- サブメッセージ: 「きが あうね!」
- ボタンエリア
- 「もういっかい」
- [ユーザー]
- [イベント5] 「もういっかい」を押した
イベント3
- [画面]
- ヤギ: 嬉しいヤギ
- メインメッセージ: 「まけ!」
- サブメッセージ: 「ほもさぴえんすって よわい いきものだね!」
- ボタンエリア
- 「もういっかい」
- [ユーザー]
- [イベント5] 「もういっかい」を押した
イベント4
- [画面]
- ヤギ: 荒ぶるやぎ
- メインメッセージ: 「じゃ〜んけ〜ん」
- サブメッセージ: 「やぎさんじゃなくて ぼたんをおしてね!」
- ボタンエリア:
- 「ぐー」
- 「ちょき」
- 「ぱー」
- [ユーザー] 「なにこれ、どれかボタン押せばいいの?」
- [イベント1] ぐーを押した
- [イベント2] ちょきを押した
- [イベント3] ぱーを押した
イベント5
初期状態と同じ
実装する
TEAは画面設計と親和性が高いので簡単に実装できます。
ポイントとなる概念は4つ:
- モデル
- 初期状態
- メッセージ/アップデート
- ビュー
モデル
アプリケーションの状態を、Elmの値で表現してみましょう。
これをTEAでは「モデル」と言います。
基本的には「画面設計」で列挙した各イベント後の状態をカスタム型で表現するだけです。
-- あとで拡張できるようにレコード型にしておくと便利!
type alias Model =
{ phase : Phase
}
type Phase
-- 初期状態/イベント5
= Start
-- イベント1
| AfterWin
-- イベント2
| AfterDraw
-- イベント3
| AfterLose
-- イベント4
| AfterClickingGoat
初期状態
初期状態のモデルを定義しましょう。
init : Model
init =
{ phase = Start
}
そのまんま
メッセージ/アップデート
イベントハンドリングのことです。
シナリオのイベントに名前をつけて、それぞれどのようにモデルを更新するか定義しましょう。
-- メッセージ (イベント)
type Msg
= PressGuu
| PressChoki
| PressPaa
| ClickGoat
| PressAgain
-- モデルの更新 (イベントが起きた時にどのようにモデルを更新するか)
update : Model -> Msg -> Model
update model msg =
case msg of
PressGuu ->
{ phase = AfterWin
}
PressChoki ->
(中略)
PressAgain ->
{ phase = Start
}
ビュー
モデルをもとに実際の画面をつくってみましょう!
elm/htmlライブラリを使います。
import Html exposing (..)
import Html.Attributes exposing (..)
view : Model -> Html Msg
view model =
{-
<div>
<img src="./img/goat/normal.jpg">
...
</div>
-}
div
[]
[ img
[ src <|
case model.phase of
Start ->
"./img/goat/normal.jpg"
(後略)
]
[]
-- メインメッセージ
, h1
[]
[ text <|
case model.phase of
Start ->
"じゃ〜んけ〜ん"
AfterWin ->
"かち!"
(中略)
AfterClickingGoat ->
"じゃ〜んけ〜ん"
]
-- サブメッセージ
, h2
[]
[ text <|
case model.phase of
Start ->
"すきな ぼたんを おしてね!"
(後略)
]
-- ボタンエリア
, if model.phase == Start || model.phase == AfterClickingGoat then
div
[]
{-
<button type="button">ぐー</button>
-}
[ button
[ type_ "button"
]
[ text "ぐー"
]
, button
[ type_ "button"
]
[ text "ちょき"
]
, button
[ type_ "button"
]
[ text "ぱー"
]
]
else
div
[]
[ button
[ type_ "button"
]
[ text "もういっかい"
]
]
]
ビューをイベントと紐付ける
これだけでは、ユーザーのどんな操作がどのイベント(Msg
)を引き起こすのかがわかりません。
Html.Events
の関数を使ってイベントの紐付けを行います。
import Html.Events as Events
div
[]
[ button
[ type_ "button"
-- ボタンを押した時のイベントを紐付ける
, Events.onClick PressGuu
]
[ text "ぐー"
]
, ...
, ...
]
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events as Events
view : Model -> Html Msg
view model =
div
[]
[ img
[ src <|
case model.phase of
Start ->
"./img/goat/normal.jpg"
AfterWin ->
"./img/goat/loser.jpg"
AfterDraw ->
"./img/goat/draw.jpg"
AfterLose ->
"./img/goat/winner.jpg"
AfterClickingGoat ->
"./img/goat/wild.jpg"
, if model.phase == Start then
Events.onClick ClickGoat
else
-- 何も属性がないときのテクニック
class ""
]
[]
, h1
[]
[ text <|
case model.phase of
Start ->
"じゃ〜んけ〜ん"
AfterWin ->
"かち!"
AfterDraw ->
"あいこ!"
AfterLose ->
"まけ!"
AfterClickingGoat ->
"じゃ〜んけ〜ん"
]
, h2
[]
[ text <|
case model.phase of
Start ->
"すきな ぼたんを おしてね!"
AfterWin ->
"ヤギさんにも ようしゃが ないね!"
AfterDraw ->
"きが あうね!"
AfterLose ->
"ほもさぴえんすって よわい いきものだね!"
AfterClickingGoat ->
"やぎさんじゃなくて ぼたんをおしてね!"
]
, if model.phase == Start || model.phase == AfterClickingGoat then
div
[]
[ button
[ type_ "button"
, Events.onClick PressGuu
]
[ text "ぐー"
]
, button
[ type_ "button"
, Events.onClick PressChoki
]
[ text "ちょき"
]
, button
[ type_ "button"
, Events.onClick PressPaa
]
[ text "ぱー"
]
]
else
div
[]
[ button
[ type_ "button"
, Events.onClick PressAgain
]
[ text "もういっかい"
]
]
]
しあげ
elm/browserライブラリを使います。
import Browser
main =
Browser.sandbox { init = init, update = update, view = view }
これをくわえるだけ!
リファクタリング
Elmは強い静的型とかしこいコンパイラによって守られているので、リファクタリングしてもデグレしにくいぞ!
関数を分けてみよう
ビューが長すぎて読めなぁあい!
別の関数に分けてみましょう。
view : Model -> Html Msg
view model =
div
[]
[ img
[ src <| goatSrc model.phase
, if model.phase == Start then
Events.onClick ClickGoat
else
-- 何も属性がないときのテクニック
class ""
]
[]
, h1
[]
[ text <| mainMessage model.phase
]
, h2
[]
[ text <| subMessage model.phase
]
, buttons model.phase
]
めちゃんこ短い!見やすい!
別の場所に定義するだけ。
{-| ヤギ画像
-}
goatSrc : Phase -> String
goatSrc phase =
case phase of
Start ->
"./img/goat/normal.jpg"
AfterWin ->
"./img/goat/loser.jpg"
AfterDraw ->
"./img/goat/draw.jpg"
AfterLose ->
"./img/goat/winner.jpg"
AfterClickingGoat ->
"./img/goat/wild.jpg"
{-| ボタンエリア
-}
buttons : Phase -> Html Msg
buttons phase =
case phase of
Start ->
guuChokiParrButtons
AfterClickingGoat ->
guuChokiParrButtons
_ ->
againButtons
guuChokiParrButtons : Html Msg
guuChokiParrButtons =
div
[]
[ button
[ type_ "button"
, Events.onClick PressGuu
]
[ text "ぐー"
]
, button
[ type_ "button"
, Events.onClick PressChoki
]
[ text "ちょき"
]
, button
[ type_ "button"
, Events.onClick PressPaa
]
[ text "ぱー"
]
]
シナリオの改善
失敗シナリオ
- ヤギの写真とともに「じゃ〜んけ〜ん」と画面に表示され、「ぐー」「ちょき」「ぱー」のボタンが並んでいる
- 「じゃんけんゲームってことかな?」
- とりあえず「ぐー」を押してみた
- 「かち!」と表示され、ヤギが悔しがった顔をしながら「ヤギさんにも ようしゃが ないね!」と言ってくる
- 「ん?? ヤギが負けて悔しがってるっぽいのに『かち!』っておかしくない?バグ?」
誰の視点で「かち!」と言っているかわかりにくいのが問題。
「あなたの かち!」に変えるだけでもいいけど、せっかくなのでユーザーの名前を聞いてみよう!
シナリオex1
- 「あなたの名前を教えてください」という案内とともに入力欄が表示されている
- 「なにこれ、個人情報とられそうで怖い |> _ <|」
- よく見たら「結果の表示に使います」と書いてある
- 「なんだ、じゃあ仮名でもいいのか」
- 「ウラジーミル・ヤギスキィ」と入力して「決定」ボタンを押した
- ヤギの写真とともに「じゃ〜んけ〜ん」と画面に表示され、「ぐー」「ちょき」「ぱー」のボタンが並んでいる
- 「じゃんけんゲームってことかな?」
- とりあえず「ぐー」を押してみた
- 「ウラジーミル・ヤギスキィさんの かち!」と表示され、ヤギが悔しがった顔をしながら「ヤギさんにも ようしゃが ないね!」と言ってくる
- (後略)
画面設計(改)
基本画面構成
設定画面
- メインメッセージ: ユーザーへのメインのメッセージを表示する
- サブメッセージ: ユーザーへの補助的なメッセージを表示する
- 入力欄: 入力ボックスや送信ボタンを表示する
ゲーム画面
- ヤギ: ヤギの写真が表示される
- メインメッセージ: ユーザーへのメインのメッセージを表示する
- サブメッセージ: ユーザーへの補助的なメッセージを表示する
- ボタンエリア: ボタンを表示する
初期状態
- [画面] 設定画面
- メインメッセージ: 「あなたの名前を教えてください」
- サブメッセージ: 「結果の表示に使います」
- 入力欄:
- 名前入力欄
- 「決定」ボタン
- [ユーザー] 「仮名にしよ」
- [イベントex1] 名前(=
name
)を入力してフォーカスを外した
- [イベントex1] 名前(=
イベントex1
- [画面] 設定画面
- メインメッセージ: 「あなたの名前を教えてください」
- サブメッセージ: 「結果の表示に使います」
- 入力欄:
- 名前入力欄
- 「決定」ボタン
- [ユーザー] 「これでよし!」
- [イベントex2] 「決定」ボタンを押した
イベントex2
- [画面] ゲーム画面
- ヤギ: ノーマルやぎ
- メインメッセージ: 「じゃ〜んけ〜ん」
- サブメッセージ: 「すきな ぼたんを おしてね!」
- ボタンエリア:
- 「ぐー」
- 「ちょき」
- 「ぱー」
- [ユーザー] 「なにこれ、どれかボタン押せばいいの?」
- [イベント1] ぐーを押した
- [イベント2] ちょきを押した
- [イベント3] ぱーを押した
- [イベント4] ヤギを押した
イベント1'
- [画面]
- ヤギ: 悔しそうなやぎ
- メインメッセージ: 「
name
さんの かち!」 - サブメッセージ: 「ヤギさんにも ようしゃが ないね!」
- ボタンエリア
- 「もういっかい」
- [ユーザー] 「ああ、ヤギさんだからチョキしかだせないのか」
- [ユーザー] 「今度は負けてあげよう」
- [イベント5] 「もういっかい」を押した
実装する(改)
モデル
フェーズを変更し、変数name
も保持できるようにしましょう!
-- あとで拡張できるようにレコード型にしておいてよかった!
type alias Model =
{ phase : Phase
, name : String
}
type Phase
-- 初期状態 / イベントex1
= Setting
-- イベント5 / イベントex2
| Start
-- イベント1'
| AfterWin
-- イベント2'
| AfterDraw
-- イベント3'
| AfterLose
-- イベント4
| AfterClickingGoat
初期状態
init : Model
init =
{ phase = Setting
, name = ""
}
メッセージ/アップデート
イベントが2つ追加されます。
type Msg
= PressGuu
| PressChoki
| PressPaa
| ClickGoat
| PressAgain
-- イベントex1
| ChangeName String
-- イベントex2
| SubmitName
update : Msg -> Model -> Model
update msg model =
case msg of
-- モデルの関係ないフィールド(`name`)はそのままにしておきたいので
-- レコードの更新構文が便利
PressGuu ->
{ model
| phase = AfterWin
}
PressChoki ->
{ model
| phase = AfterDraw
}
PressPaa ->
{ model
| phase = AfterLose
}
ClickGoat ->
{ model
| phase = AfterClickingGoat
}
PressAgain ->
{ model
| phase = Start
}
ChangeName str ->
{ model
| name = str
}
SubmitName ->
{ model
| phase = Start
}
ビュー
view : Model -> Html Msg
view model =
case model.phase of
Setting ->
settingView model
_ ->
gameView model
入力欄のイベントには elm-community/html-extra
の Html.Events.Extra.onChange
が便利。
import Html.Events.Extra as Events
settingView : Model -> Html Msg
settingView model =
div
[]
[ h1
[]
[ text "あなたの名前を教えてください"
]
, h2
[]
[ text "結果の表示に使います"
]
, input
[ type_ "text"
, value model.name
, Events.onChange ChangeName
]
[]
, button
[ type_ "button"
, Events.onClick SubmitName
]
[ text "決定" ]
]
さらに学ぶには
公式ガイドの日本語訳 がとっても便利!