旧gaaamiiのブログ

間違ったことを書いている時があります。コメントやTwitter、ブコメなどでご指摘ください

基礎からわかるElmを読んだ感想

1年半くらい前に注文したものの、Elmのバージョンが0.18->0.19に上がる都合とかで出版予定日が延びていたElm本がついに昨日、自宅に届いていた。感動した。

勉強になったところとか、感想を書いていく。

基礎からわかる Elm

基礎からわかる Elm

Amazon

型の話

Elmといえば静的型付け言語であり、No Runtime Errorなウェブアプリを書けるという良さがある。 一方で、型が読めないと何が何だかわからない。型の話がわからないと、自分で型を書けないのはもちろん、なんといってもドキュメントが読めなくて困る。

この本では、50〜71ページのおよそ20ページでこのElmの型についての説明が書かれている。この辺の節を読むと、制約つきの型変数、type aliasがコンストラクタを生成すること、カスタム型とパターンマッチなどについて学べる。とても丁寧でわかりやすい。特にtype aliasとかカスタム型は自分で勉強したときよくわからなくてつらかった思い出があるので、もっと早くこの本が欲しかった。

MaybeとかResultとか

Elmにはnullとかnilってものはないという話で、その代わりMaybeって型がある。Maybeはモジュールにもなっていて、withDefaultとかmapとかandThenなどの関数が生えているのでだいたいやりたいことはできる。なんとなく自分の理解が怪しかったので復習がてら読んだ。

Resultも似たようなものだけど、Maybe以上に自分の理解が怪しかったのはおそらく、APIへリクエスト投げてレスポンス取得して云々みたいな処理を実装しようというときに初めてこのResultに出会ったからだと思う。ElmでHTTPリクエストを送る処理はHttpというモジュールでやる。そこでResultという型が出てくるのだけど、もちろんResult自体はそれ以外のところでも使えるし、Httpのためだけのものでもない。

本書では、

Resultは失敗するかもしれない結果を表すデータ構造です。

と簡潔に書かれていて、例示されているコードもHttpとかは関係のないものになっている。「ElmでAPIへリクエスト投げてデータ取得する実装をやろう」となって、「実装してみたもののResultってやつがなんだかよくわからない」となったときに本書のこの辺を読むとすっきりわかってよさそう。

描画の仕組みと高速化

Html.KeyedとHtml.lazyの話。

ReactでもkeyアトリビュートとReact.PureComponentがあり、これらと同じようなものだと思う。

ナビゲーション

onUrlRequestonUrlChangeの説明がありがたい。 ナビゲーション周りの知識はSPAつくるときに必須なので、これが簡潔にまとめられているのは助かる。

感想

Elmを使って、実行時エラーが発生しないアプリを作れるのは魅力的だけど、新たに言語覚えるのはしんどい。 日本語の書籍にまとまったことで、言語覚えるしんどさが軽減されて、Elm人気もじわじわ高まっていくんじゃないかと思った。

combineReducersってなんだ

最近、Structuring Reducersとかそこらへんのページを読んでる。

なんで読んでるのかというと、Reduxでウェブアプリケーション書いていて、reducerどう分けるのがきれいなんだっていうのがいまいちわかっていない気がしたから。Immutable.js入れときゃいいのか?とか考える前に、ここを読むべきっぽいので読もうみたいな気持ち。

その中で、combineReducersっていう関数が何をしているのか気になったので、調べた。

combineReducersは複数のreducerたちを、createStoreに渡せる一つの関数の形にしてくれるやつらしい。 実際何をしているのか見てみる。

てきとうにreduxをnode_modulesにインストールしてあるプロジェクトで、nodeのREPLを立ち上げる。

$ node
>

そんで、redux.combineReducersという関数を雑に確認

> const redux = require('redux')
undefined
> redux.combineReducers
[Function: combineReducers]
> redux.combineReducers.toString()
'function combineReducers(reducers) {\n  var reducerKeys = Object.keys(reducers);\n  var finalReducers = {};\n  for (var i = 0; i < reducerKeys.length; i++) {\n    var key = reducerKeys[i];\n\n    if (process.env.NODE_ENV !== \'production\') {\n      if (typeof reducers[key] === \'undefined\') {\n        warning(\'No reducer provided for key "\' + key + \'"\');\n      }\n    }\n\n    if (typeof reducers[key] === \'function\') {\n      finalReducers[key] = reducers[key];\n    }\n  }\n  var finalReducerKeys = Object.keys(finalReducers);\n\n  var unexpectedKeyCache = void 0;\n  if (process.env.NODE_ENV !== \'production\') {\n    unexpectedKeyCache = {};\n  }\n\n  var shapeAssertionError = void 0;\n  try {\n    assertReducerShape(finalReducers);\n  } catch (e) {\n    shapeAssertionError = e;\n  }\n\n  return function combination() {\n    var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n    var action = arguments[1];\n\n    if (shapeAssertionError) {\n      throw shapeAssertionError;\n    }\n\n    if (process.env.NODE_ENV !== \'production\') {\n      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);\n      if (warningMessage) {\n        warning(warningMessage);\n      }\n    }\n\n    var hasChanged = false;\n    var nextState = {};\n    for (var _i = 0; _i < finalReducerKeys.length; _i++) {\n      var _key = finalReducerKeys[_i];\n      var reducer = finalReducers[_key];\n      var previousStateForKey = state[_key];\n      var nextStateForKey = reducer(previousStateForKey, action);\n      if (typeof nextStateForKey === \'undefined\') {\n        var errorMessage = getUndefinedStateErrorMessage(_key, action);\n        throw new Error(errorMessage);\n      }\n      nextState[_key] = nextStateForKey;\n      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;\n    }\n    return hasChanged ? nextState : state;\n  };\n}'

てきとうなreducerを渡して何がかえってくるかを見てみる

> redux.combineReducers({ hoge: (state, action) => { return state }})
[Function: combination]

関数が返ってきたので、これのなかみを雑に見る。

> redux.combineReducers({ hoge: (state, action) => { return state }}).toString()
'function combination() {\n    var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n    var action = arguments[1];\n\n    if (shapeAssertionError) {\n      throw shapeAssertionError;\n    }\n\n    if (process.env.NODE_ENV !== \'production\') {\n      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);\n      if (warningMessage) {\n        warning(warningMessage);\n      }\n    }\n\n    var hasChanged = false;\n    var nextState = {};\n    for (var _i = 0; _i < finalReducerKeys.length; _i++) {\n      var _key = finalReducerKeys[_i];\n      var reducer = finalReducers[_key];\n      var previousStateForKey = state[_key];\n      var nextStateForKey = reducer(previousStateForKey, action);\n      if (typeof nextStateForKey === \'undefined\') {\n        var errorMessage = getUndefinedStateErrorMessage(_key, action);\n        throw new Error(errorMessage);\n      }\n      nextState[_key] = nextStateForKey;\n      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;\n    }\n    return hasChanged ? nextState : state;\n  }'

中身を見ると、なるほどたしかにstateのキーごとにreducer(state, action)を呼び出して、次の状態を作っている。

function combination()というのを見て、なんだ(state,action) => stateの形になってないじゃないかと一瞬思ったけど、よく見るとarguments[0]をstateに、arguments[1]をactionに代入しているので、combination()combination(state, action) と呼び出せることがわかる。関数定義で引数がなさそうでも、引数を渡して呼べて、それをargumentsでアクセスできるらしい1

呼び出してみる。

> const combination = redux.combineReducers({ hoge: (state, action) => { return state }})
undefined
> combination(1, {})
Error: Reducer "hoge" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.

エラーになった。さすがに雑すぎたようだ。このエラー文で言われている通り、初期状態をundefinedにしたらいけないので、そこらへんもちゃんとしたreducerを渡して再度試す。

> const myCombination = redux.combineReducers({ hoge: (state = 0, action) => { return state }})
undefined
> myCombination({ hoge: 1}, {type: 'HOGE_ACTION', payload: 'hogehoge'})
{ hoge: 1 }
>

できた。雰囲気としてactionぽいものを渡しているけど、reducerで利用していないので、空のオブジェクトでも呼び出せる。

> myCombination({ hoge: 1}, {})
{ hoge: 1 }

以上で、combineReducersがほんとにただ複数のreducerをまとめるだけの普通のヘルパー関数であることがわかった。よかった。

Pageコンポーネントの共通処理をHOCにする

Reactでは継承よりも、HOCを使うのが推奨されている。HOCとはHigher Order Componentの略で、コンポーネントを引数にとってコンポーネントを返す関数のこと。

なので、たとえば似たような前処理をするページコンポーネントが複数あったとして、その共通処理を書く場合、

export default class UsersDetailPage extends UsersPage { ... }

みたいなことはせず

export default enhanceUsersPage(UsersDetailPage)

みたいな感じになる。

なんで継承じゃなくて関数...?という疑問は当然出てくるけど、以下に

Note that a HOC doesn’t modify the input component, nor does it use inheritance to copy its behavior. Rather, a HOC composes the original component by wrapping it in a container component. A HOC is a pure function with zero side-effects.

reactjs.org

と書かれて強調されているように、HOCはもとのコンポーネントの振る舞いは一切変えない。

HOCにコンポーネントを渡せば振る舞いが追加されたコンポーネントとして使えるし、HOCに渡さずそのままコンポーネントを使うこともできる。

React Routerを使って、イベントが起きたときにURLを変えつつ何か処理をする

SPAを作っていると、何かイベントが起きたときにURL変えつつなにか処理をしたいということがあります。 たとえば絞り込み検索で、クエリストリングだけ書き換えて画面更新したいというケース。具体的には/blog_posts から /blog_posts?tag=1 へ遷移するような感じです。history.listenにコールバックを渡すと、これが実現できたのでメモしておきます。

まずは実装例

interface Props extends RouteComponentProps {
  hoge: string;
}

class MyPage extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props)
  }

  componentDidMount() {
    this.unregisterHistoryCallback = this.props.history.listen((location: LocationDescriptor) => {
      this.updateMyPageData(location.search.toString())
    })
  }
  
  componentWillUnmount {
    this.unregisterHistoryCallback()
  }
  
  private updateMyPageData(query: string) {
    // ...
  }

  private pushHistory(obj: any) {
    const query = QueryHelper.toQueryString(obj)
    this.props.history.push({ search: query })
  }
}

やっていること

URLつくる -> URLに紐付いた状態更新メソッド的なもの(updateMyPageData)を呼び出している。

やっていること詳しく

ポイントは、URLが変わったときに呼ぶコールバックの登録と解除。それを history.listen というメソッドで行っている。listenは、コールバックを登録してそれを解除する関数を返すので、登録時の返り値を保持しておいて、コンポーネントのunmountのタイミングでそれを呼べば良い。

追記

上記のやり方では props.match.params (たとえばURLが /users/{userId}/posts?hoge=fugauserIdの部分)を、最新のものを参照できなくて悩んでたのですが、以下の記事を読んで、React Routerのlocationを使えばいいことを知りました。

qiita.com

参考

overflow: visible scroll がうまいこといかなくてつらい

https://codepen.io/gaaamii/pen/VgrwMvcodepen.io

overflow: visible scroll;

と書いたらx方向ははみ出て見えて、y方向はスクロールになってほしいのだけどそうはいかなくてつらいという話。なんでx方向もscrollになるんだ。

コンビニ人間を読んだ感想

短くてパッと読めて面白かった。主人公が随分変わった人だったけど、共感できるところもあった。生きるの難しいけど、なにか一つでも没頭できる行為なりなんなりがあるのはいいことですな、という気持ちになった。

styled-componentsの良さがよくわからない

と、煽ってるような記事タイトルだけど、 煽りたいわけではなく、単にわからないというつぶやきです。

自分はいまCSS Modulesでやっていて、特に困ることがない。

最近styled-componentsのほうがよく聞くのはなんでだろう。 あっちのほうがなにかいけてるポイントがあるんだろうか。

2018年の振り返り

今年も色々あった。時間過ぎるのがあっという間で、気付いたら10年くらい経ってしまいそうなのでこうして思い出(?)をブログに残しておく。

1月

Reactを書いてた。Angularで書いたものをReact化するという少し変わった仕事だった。他にやるべきことはないか、なぜこれをするのか、という考えが足りなかった気がする。これをやりましょうというのを提案するべきだった。

2月

引き続きReactを書いていた。 組織的には、人がどんどん辞めていった時期。

3月

3月も人が辞めていた気がする。1〜3月の辞め具合がすごかったのでもはや何月に誰が辞めたか正確に覚えていない。

ここで、自分がそれまで携わっていたプロジェクトは開発を止めて、自分は異動する話になった。運良くやりたいことができるところへ異動になり、クビも切られなくてよかった。さすがにクビ切るというのは極端だけど、一瞬その会社での仕事がなくなったのは事実で、いろいろ考えた時期だった。

4月

新部署での仕事が始まる。ちゃんとしたRailsアプリケーションの開発を仕事でやるのは入社したばかりの頃以来で、ひたすら力不足を感じていた。あと会社が資金調達した。

5月

相変わらずひいひい言って大変だった。

6月

わりと大事な機能を開発することになり、これも大変だった。リーダーのレビューが最強で最高で学びが多かった。 難しかったのはオブジェクトの責務分けとか、RESTとして正しいURI設計をするだとか、たくさんテーブルを繋げて条件を指定するようなSQLの書き方だとか。あとRSpecもなかなかうまいこと書けなくて最初は特につらかった。

サーバーサイドの開発をするとき、esaでシーケンス図(UML)を書けて、それが考えを整理したり共有する上でとても役に立った。

7月

shgam.hatenadiary.jp

shgam.hatenadiary.jp

悩んでいる感じがこのブログにも残されている。今思うと、余裕がなかったとはいえwebpackerの設定直さないまま開発を続けていたのはとても良くなかった。

開発していた機能のリリースなどをした。

8月

React, Reduxなプロジェクトの雛形を用意してつく始めたり、Elmのあれをあれしたりしていた。

9月

仕事は引き続き、ReactとReduxのあれをあれしていた。

buildersconへ行ったり、ISUCONに出たりした。

shgam.hatenadiary.jp

shgam.hatenadiary.jp

10月

同じくReactの云々。

11月

Elmで途中まで作っていたものをデモでお披露目するということで、そちらを急いで進めたりなどした。その後のリリースまで持ってくのもけっこう大変で、Elm力が高まった気がする。スケジュール的には押してしまって反省あるのみ。

あと、RubyWorld Conferenceへ行った。

shgam.hatenadiary.jp

あと、結婚したり、歳をとったりもした。

12月

Elmで書いてたやつがリリースされ、Elmのアドベントカレンダーにも記事を投稿したりした。

qiita.com

その後は引き続きReactを書いてた。

会社が御茶ノ水へ移転した。それまでと比べて、戸惑うくらい快適なオフィスになった。 代々木もそれなりに長くいたので感慨深かった。またいつかいそじ行きたい。

shgam.hatenadiary.jp

一年通して

仕事全般について

1〜3月は会社が良い雰囲気ではなかった。業界的に人が辞めるのはよくあることかもしれないけど、それにしてもこんなに一気に…という感じだった(年の後半では人が増えてオフィスも移転して、雰囲気も明るくなって本当に良かった)。 当時は、困った、さあこれからどうするって感じで、そこで自分が関わってきたところも、問題を洗い出したりして、開発を止めることになり、そこを離れるということになった。

その時は、ああこうやって仕事って無くなるんだなと、はっきりとした危機感を持った。 事業的にいろいろ整理が発生するのは絶対あることだろうけど、突然仕事が無くなったときに、どこにも行き場がないみたいな状態にならないように精進していかなくてはと思った。今回に関しては運が良く、希望のところへ異動させてもらえた。

ウェブフロントエンドについて

自分の仕事に危機感を持ったこともあり、ウェブフロントエンドに関して考えることが多かった。 そこで考えて出てきたのは、やっぱりウェブアプリケーション使うなら、使いやすく、速く感じられるのが良いだろうということ。そのために必要な技術がウェブフロントエンドの諸々だと思っている。

iOSAndroidのネイティブアプリだったら、はじめからサーバー側のロジックととクライアント側のロジックは切り離されてるけど、古くから(クライアントMVCとかSPAが持て囃される以前)のウェブアプリケーションだと、サーバー側のコードの一部として管理されていて、密結合だったりする。すると、クライアント側での最適化みたいなのは難しく、基本的にユーザーは毎回サーバーから手元のブラウザへ、HTMLドキュメント全体をダウンロードして、画面を更新することになる。

Fluxみたいなパターンと仮想DOMみたいな仕組みで、ユーザーの行動に応じて画面のなかの必要な分だけ更新するようなアプリケーションが可能になった。ただウェブにはURLがあるので、フロントエンド側にルーターを持ち、ウェブのURLの表現を保つ工夫も必要になる。

もちろんSPAみたいなのは良いことばかりではなく、サーバーがリクエストに応じたHTMLドキュメントを返すだけというシンプルさを捨てることになる。実装の複雑度は増すし、ユーザーからの目線で考えてみても、SPAのほうが体験が良いと言えるのはちゃんと作られている場合だけで、不具合があったり不自然な画面遷移をしたりするものを作ってしまうと、SPAなんかしなけりゃよかったということになりかねないとは思う。それでも、自分はサーバーとクライアントは分けたほうが良いと思っている。APIと画面を分けるのは、サーバーはサーバーを、クライアントはクライアントのことをうまくやるのに集中できる環境をつくることだと思う。

しかし、モバイルで最高の体験を、と考えるならやっぱりウェブよりネイティブアプリだと思っていて、じゃあ上で書いたようなものは何をつくるために必要なのかと考えると、PCで使うSaaSみたいなものがあってると思う。「クラウドなんちゃらシステム」みたいなもの。そして、人がパソコンのキーボードを叩くのは多くが仕事かそれに近いようなときで、それ以外はみんなスマホを使ってる(と思う。すごい雑認識だけど)。だから、ウェブフロントエンドをさらに個人的な視点で狭く言うと、仕事でPCで使うときに使うシステムを使いやすくする技術になる。

今まさにやっている仕事もそれに近いものなので、これをどこまでの出来にできるかというのが、事業にも、自分のキャリアにも重要になっていると思ってる。なかなかうまくいかないこともあるけど、引き続き頑張っていきたい。

プログラミング言語について

Ruby(Rails)は、よく考えてる人が書いたものは洗練されていて、責務がしっかり分けられていてきれいという感想を持った。オブジェクト指向の本読みきらねばという気持ちが強まった。

あと、一時期Elmを書いたことにより以下のような気持ちが芽生えた。

  • 静的型付け言語良い
  • Lintとかなるべく自分で設定する必要がなく、コードのフォーマットとかは自動でされたほうが嬉しい

関数型言語についての感想は、関数の入力の型がその入力を制限していて、同じ入力に対して常に同じ出力になるっていう単純さが良くて、ほかの難しそうなことについては正直よくわかっていない。

特にオブジェクト指向デザインパターンのような、プログラム全体の整理整頓みたいなのをどうやるべきかというのがまだよくわかってないけど、Elmの場合は中心となる型があり、それをいじくる関数が一緒にまとまったものがモジュールになるという考え方っぽかった。

来年はGo言語も少し触ってみたいという気持ちになってる。

健康

大きい病気はしなかったけど、地味に疲れやすい体になってきてるなという気はした。 健康はなんかぼーっとしてるとどんどん奪われていくので、きっと健康オタクくらいの感じがちょうどいい。

生活

今年は結婚をしたので、人生設計を堅くしてこれからのことに備えていきたい。

人間関係

ありがたいことに、仕事でも親族関係でも友人関係でも、嫌いな人と接することがほぼない。とても恵まれていた。

インターネット上での活動

Podcastとかほとんどとれなかった。

きまべんという思いつきで始めたScrapboxプロジェクトが、熱意のある方々によって続けられていて、自分もやっていかなくてはと刺激される。

まとめ

今年もお世話になりました。来年もよろしくお願いします。

レイアウトのためのatomコンポーネントとかtemplates層とかの実装

Atomic Designの本「Atomic Design ~堅牢で使いやすいUIを効率良く設計する」を参考にしながら、こういう感じでやっていってる。

// atoms/AppLayout.tsx
const AppLayout = (props: Props) => {
  const children = React.Children.toArray(props.children)

  return (
    <div className={styles.left}>{children[0]}</div>
    <div className={styles.right}>{children[1]}</div>
    <main>{children[2]}</main>
  )
}

// templates/UserPageTemplate.tsx
interface Props {
  user: UserInterface;
}

const UserPageTemplate = (props: Props) => {
  return (
    <AppLayout>
      <AppHeader />
      <AppSidebar />
      <UserView user={props.user} />
    </AppLayout>
  )
}

AppLayout に当てるCSSのflexboxなりgrid layoutなりでレイアウトを実現しておいて、同じレイアウトのページを作成するときは同じようにAppLayoutで囲えばそのレイアウトになるという感じになる。

templateのコンポーネントでは、どのレイアウトにどのOrganismを置くかということを記述する。URLから最初の状態を作ってpropsとして渡すのはその上のpageコンポーネントにやってもらう。なのでtemplateはStateless Functional Component (状態無し関数コンポーネント)になる。

レイアウトがAtomだってことに最初はちょっと違和感あったけど、別に何もおかしなことはなかった。

代々木の思い出

オフィス移転のため、今日が代々木オフィス最終日だった。

今の会社に転職してから色々あったけど、いそじというラーメン屋のつけ麺がずっと安定して美味かった。

26歳

忘れてた。26歳になっていました。若者を自称するのはもはや不可能になり、身体能力も着実に衰えてきています。この前テニスしたら大学生にロブで振られてボロボロにされ、負けました。翌々日の筋肉痛が全てを物語っていました。つらい。26歳も頑張ります。

キウイの本を読んでる

キウイ🥝。

なんで読んでるのか

自分はいまウェブアプリケーションのUIをつくる仕事をしているので、ReactやらReduxの記事とかはよく読む。けどもう少し広めの、UIをつくるプログラミング全般に適用できる設計パターンみたいなのを学びたかった。

あとで感想ここに書く

Storybookを使って気付くCSSの粗さ

がっと作った大きなOrganismがあって、それを構成するMolelculeやらAtomやらのレベルのコンポーネントをすべてStorybookに置いた段階で、「あれ、なんかこれいい感じに表示できない...」ということに気付く。CSS Modulesを使ってスタイルのスコープをそのコンポーネントに閉じさせているものの、それだけでは期待する見た目にならない。正しい見た目が上の要素に依存していて、それがないとちゃんとした見た目にならない。直さなくては...ということになる。

Storybookに細かい単位でコンポーネントを置かなければこういうのをごまかせてしまうので、いざ細かいコンポーネントを使い回すときになるまで「なんだこれ」ということに気づかなかったりする。Storybookの使いみちはいろいろあるけど、こういうスタイルのスコープ確認につかえて便利だと思う。