Haskellの言語機構整理
はじめに
「すごいHaskellたのしく学ぼう!」を読んだ。Haskellは完全に初体験。本の中で紹介されていたHaskellの機能で、印象的だったものを整理しておく。
セクションタイトルの「基本的」「発展的」は自分独自の主観的な分類。それ単体で完結する単純な機能は基本に入れている。一方で自分が初見で理解しづらいと感じ、概念そのものを受け入れるのに時間がかかったものは発展の方に入れている。
基本的な機能
リスト内包表記
[x * 2 | x <- 1..5] > [2, 4, 6, 8, 10]
パターンマッチ
factorial :: Int -> Int factorial 0 = 1 factorial n = n * factorial(n-1)
ガード
factorial :: Int -> Int factorial n | factorial 0 = 1 | factorial n = n * factorial(n-1)
where節
calcBmis :: [(Double, Double)] -> [Double] calcBmis xs = [bmi x h | (x, h) <- xs] where bmi weight height = weight / height ^ 2
let式
4 * (let a = 9 in a + 1) + 2 > 42
let で値を名前に束縛し、in の中の式が評価される。
case式
describeList :: [a] -> String describeList ls = "The list is " ++ case ls of [] -> "empty." [x] -> "a singleton list." xs -> "a longer list."
ラムダ式
map (\(a, b) -> a + b) [(1, 2), (3, 5)] > [3, 8]
型シノニム
type IntMap v = Map Int v -- 以下の定義と同じ type IntMap = Map Int
発展的な機能
関数適用演算子
定義
($) :: (a -> b) -> a -> b f $ x = f x
使い方
sum(filter(>10) (map (*2) [2..10])) -- 以下のように書き換えられる sum $ filter(>10) (map (*2) [2..10]) sum $ filter(>10) $ map (*2) [2..10]
右側の式を左側の関数の引数として、括弧を減らすことができる。
関数合成
定義
(*) :: (b -> c) -> (a -> b) -> a -> c f . g -> \x -> f (g x)
使い方
map (\x -> negate (abs x)) [5, -3, -6] -- 以下のように書き換えられる map (negate . abs) [5, -3, -6]
データ型の定義
data Shape Circle Float Float Float | Rectangle Float Float Float Float deriving(Show) -- データ型のインスタンスを導出 -- レコード構文(フィールドに名前を与える) data Car = { company :: String , model :: String , year :: Int } deriving(Show)
型コンストラクタ
-- aは型引数 data Maybe a = Nothing | Just a
型クラスの定義
- a は型変数 class Eq a where -- 関数の型定義 (==) :: a -> a -> Bool (/=) :: a -> a -> Bool -- 関数のデフォルト実装(必須でない) (==) = not(x /= y) (/=) = not(x /= y)
型変数は対象クラス(今回はEq)のインスタンスになる型。型引数とは別。
インスタンスの定義
instance Eq TrafficLight where Red == Red = True Blue == Blue = True Yellow == Yellow = True _ == _ = False -- インスタンスになるデータ型の定義 data TrafficLight = Red | Blue | Yellow
ここで言葉の整理をしておく。
- 型クラス
- 振る舞いを定義するインターフェース(振る舞い=メソッド)
- インスタンス
- 型クラスの振る舞いを実装したデータ型
- 「データ型Aが型クラスBに属している」=「型Aは型クラスBのインスタンスである」
Functor型クラス
定義
class Functor f where fmap :: (a -> b) -> fa -> fb
使い方
instance Functor Maybe where fmap f (Just x) = Just(f x) fmap f Nothing = Nothing
Applicative型クラス
定義
class (Functor f) => Applicative f where pure :: a -> f a (<*>) :: f(a -> b) -> f a -> f b
使い方
instance Applicative Maybe where pure = Just Nothing <*>_ = Nothing (Just f) <*> something = fmap f something
Monad型クラス
定義
class Monad m where return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b -- 別名: バインド (>>) :: m a -> m b -> m b x >> y = x >>= \_ -> y -- ユーザーがfailを呼ぶことはない fail :: String -> m a fail msg = error msg
使い方
instance Monad Maybe where return x = Just x Nothing >>= f = Nothing Just x >>= f = f x
ここで Functor, Applicative, Monad の整理をしておく。
| 入力 | 出力 | 適用関数 | 実装メソッド | |
|---|---|---|---|---|
| Functor | f a | f b | a -> b | fmap :: (a -> b) -> f a -> fb |
| Applicative | f a | f b | f(a -> b) | pure :: a -> f a (<*>) :: f(a -> b) -> f a -> fb |
| Monad | m a | m b | a -> m b | return :: m -> m a (>>=) :: a -> m b -> m a -> m b |
<$> (fmapと等価な中置演算子)
定義
(<$>) :: (Functor f ) => (a -> b) -> f a -> f b f <$> x = fmap f x
使い方
pure f <*> x <*> y -- 以下のように書き換えられる fmap f x <*> y f <$> x <*> y
do記法
marySue :: Maybe Bool marySue = Just 9 >>= (\x -> Just(x > 8)) -- 以下のように書き換えることができる marySue = do x <- Just 9 Just (x > 8)
参考
- すごいHaskellたのしく学ぼう!
- 動作環境などはこちらを参考にした記事
- モナドのイメージ的な理解の助けになった記事
VSCodeのGit連携
前提環境
要求
Gitを使用するにあたり、VSCodeに求めたいのは以下のような項目。
- 基本的な操作はコマンドでやりたい(add, commit, push, fetch, pull, stash, revert 等)
- エディタの支援を受けたいこと
- 一部ファイルのadd/reset
- diffの確認
- ファイルや行ごとのコミットログ確認
- コミットにGitHubページを開く
- コミットIDで変更内を検索
- branchの関係性を可視化
やること
デフォルトでできること
以下のことは拡張なし元からできるようになっている。
- 一部ファイルのadd/reset
- diffの確認
アクティビテーバーのソースコントロールビューから操作する。追加設定などは不要。
GitLensでできること
以下のことは拡張機能GitLensを入れることで可能になる。
- ファイルや行ごとのコミットログ確認
- コミットにGitHubページを開く
- コミットIDで変更内容を検索
エディターアクションやアクティビテーバーのGitLensビューやから操作する。また行ごとのコミットログは常にエディタ上に表示される。追加設定などは不要。
GitHistoryでできること
以下のことは拡張機能GitHistoryを入れることで可能になる。
- branchの関係性を可視化
エディターアクションのアイコンから可視化されたブランチを表示できる。追加設定などは不要。
VSCodeで React を書くための準備(TypeScript)
はじめに
前回の記事では、JavaScritpでReactを使うための環境を作成した。今回はTypeScriptでReactを使うための設定をVSCodeで行う。方針も同じで、ESLintとPrettierを使用する。
手順
まず前回のJavaScriptのときと同じnpmパッケージをインストールする(babel-eslint のみ不要)。
$ npm install --save-dev eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react-redux
加えて以下のパッケージを追加する。
$ npm install --dev-save @typescript-eslint/parser @typescript-eslint/eslint-plugin
settings.jsonに以下の内容を追記する。
"[typescript]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } }, "[typescriptreact]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } },
.eslintrc.jsは以下のようになる。
module.exports = { env: { es6: true, browser: true, }, parser: "@typescript-eslint/parser", parserOptions: { sourceType: "module", }, plugins: [ "react-redux", "@typescript-eslint", ], extends: [ "eslint:recommended", "plugin:react-redux/recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:prettier/recommended", "prettier/@typescript-eslint", ], rules: { "prettier/prettier": [ "error", { singleQuote: true, semi: true, }, ], }, };
JavaScriptのときからの変更点は以下の通り。
parserが"@typescript-eslint/parser"に変更pluginsに"@typescript-eslint"を追加extendsに"plugin:@typescript-eslint/recommended"と"plugin:@typescript-eslint/eslint-recommended"、"prettier/@typescript-eslint"を追加
参考
ReactでHooksを使う
はじめに
React 16.8 で追加された機能フック(hooks)を使ってみる。以前の記事で書いたWebsocketを使用したコードを、フックによってリファクタリングする。
今回はステートフック(useState)と副作用フック(useEffect)を使用する。これらのReactで用意されたデフォルトで使えるフックを組み合わせて、既存コードの多くの部分のをカスタムフックとして抽出し、再利用可能なものとする。
Websocketでの適用例
今回の実装はをまとめたものはこちら。
もとのコンポーネント
まずこちらが、クラスコンポーネントで書かれた既存のコード。constructorでチャネルを初期化し、アマウント時に購入の終了処理を行っている。
class App extends React.Component { constructor(props) { super(props); this.state = { messages: [], }; this.handleClick = this.handleClick.bind(this); this.endpoint = "ws:localhost:3000/cable"; this.cable = Actioncable.createConsumer(this.endpoint); this.chatChannel = this.cable.subscriptions.create( { channel: "BroadcastChannel", }, { connected: () => { console.log("connected."); }, received: (data) => { this.setState({ messages: [...this.state.messages, data], }); }, } ); } // TODO: unsubscriobe(); handleClick() { this.chatChannel.perform("speak", { message: "hoge", }); } render() { const items = this.state.messages.map((message, i) => { return <li key={i}>{message}</li>; }); return ( <div> <button onClick={this.handleClick}>message</button> <ul>{items}</ul> </div> ); } }
関数コンポーネント化
フックは関数コンポーネントでしか使用することができない。そのためコードを関数コンポーネントに書き直す。受信メッセージ文字列のリスト管理と、Websocketのチャンネルオブジェクトの管理のためにuseStateを使用している。またuseEffectにより、コンポーネントのマウント時にチャンネル購読の開始、アマウント時に終了をする処理を実現している。
一点注意しておきたいのは、購読オブジェクトを生成の中で受信時に実行される処理の記述部分(received: (data) => {...}のところ)。setMessagesの引数として、新しい値そのもではなく関数を渡している。コールバックの中ではそれが生成された時点での古い値が参照されるため、最新の状態として更新できるよう、更新方法を記述した関数を渡す必要がある。
フックに関するよくある質問 – React フック API リファレンス – React
const App = (props) => { const endpoint = "ws:localhost:3000/cable"; const cable = Actioncable.createConsumer(endpoint); const [messages, setMessages] = useState([]); const [chatChannel, setChatChannel] = useState(null); useEffect(() => { const c = cable.subscriptions.create( { channel: "BroadcastChannel", }, { connected: () => { console.log("connected."); }, received: (data) => { // 引数として関数を渡す setMessages(m => [...m, data]) }, } ) setChatChannel(c); return () => { c.unsubscribe(); } }, []); const handleClick = () => { chatChannel.perform("speak", { message: "hoge", }); } const items = messages.map((message, i) => { return <li key={i}>{message}</li>; }); return ( <div> <button onClick={handleClick}>message</button> <ul>{items}</ul> </div> ); }
カスタムコンポーネントの作成
最後にフックを使用したWebsocketの設定に関する部分の記述を、カスタムフックuseWebsocketとして切り出す。
カスタムフックの定義
export const useWebsocket = (handler) => { const endpoint = "ws:localhost:3000/cable"; const [channel, setChannel] = useState(null); useEffect(() => { const cable = Actioncable.createConsumer(endpoint); const c = cable.subscriptions.create( { channel: "BroadcastChannel", }, { connected: () => { console.log("connected."); }, received: (data) => { handler(data); }, } ) setChannel(c); return () => { c.unsubscribe(); } },[]); return channel; }
コンポーネント内での使用
コンポーネントからはuseWebsocketを、メッセージ受信時に実行させたいコールバック関数とともに呼び出すだけで、Websocketのチャンネル購読に関する設定は完了する。
const App = () => { const [messages, setMessages] = useState([]); const channel = useWebsocket((data)=>{ setMessages(m => [...m, data]); }); const handleClick = (chatChannel) => { if (chatChannel == null) { return; } chatChannel.perform("speak", { message: "hoge", }); } const items = messages.map((message, i) => { return <li key={i}>{message}</li>; }); return ( <div> <button onClick={() => handleClick(channel)}>message</button> <ul>{items}</ul> </div> ); }
Reactでルーティングを実装する(connected-react-router)
はじめに
以前スタンダードだった react-router-redux は現在非推奨になっている。現在は connected-react-router を使用することが勧められているでので、今回はこちらを使用。
手順
アプリケーション独自で作成した複数のReducerと一緒に、ルーティング用のReducer(connectRouter)を結合する。combineReducers ではReducerそれぞれに任意のキーを設定するが、ルーティング用のReducerのキーは必ずrouterとする。
import { combineReducers } from 'redux'; import { connectRouter } from 'connected-react-router'; import aReducer from './a'; import bReducer from './b'; const createRootReducer = (history) => combineReducers({ router: connectRouter(history), a: aReducer, b: bReducer });
createStore では、routerMiddlewareもミドルウェアとして与える必要がある。routerMiddlewareにはconnectRouterに与えるのと同じhistoryオブジェクトを引数として渡す。
import { createBrowserHistory } from 'history'; import { applyMiddleware, compose, createStore } from 'redux'; import { routerMiddleware } from 'connected-react-router'; import createRootReducer from './reducers'; export const history = createBrowserHistory(); export default function configureStore(preloadedState) { const store = createStore( createRootReducer(history), preloadedState, compose( applyMiddleware( routerMiddleware(history), ), ), ) return store }
結合したReducerの初期状態は、combineReducersで設定したのと同じキーを与えてプレーンオブジェクトを作成し、createStoreの引数として渡す。
import { Provider } from 'react-redux'; import { Route, Switch } from 'react-router'; import configureStore, { history } from './configureStore'; import A from './components/A'; import B from './components/B'; const initialState = { a: { name: 'hoge', logs: [] }, b: { ok: false, } } const store = configureStore(initialState); ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> <> <Switch> <Route exact path="/a" render={() => <A />} /> <Route exact path="/b" render={() => <B />} /> </Switch> </> </ConnectedRouter> </Provider>, document.getElementById("root") );
connected-react-router で用意されたアクションpushを使用してページ遷移ができる。コンテナとプレゼンテーション、それぞれのコンテナで以下のように記述する。
import { push } from "connected-react-router"; ... const mapDispatchToProps = (dispatch) => { return { redirectToB: () => { dispatch(push('/b')); }, } }; export default connect(mapStateToProps, mapDispatchToProps)(A);
const Status = (props) => ( <div> ... <button onClick={() => props.redirectToB()}>Go to B</button> </div> );
参考
意図せずリポジトリが入れ子になった場合の対処方法
発生した問題
リポジトリの特定のサブディレクトリ以下の編集がGitに管理されなかった。リポジトリのディレクトリ構成は以下の通り。
./ ├─ .git ├─ .gitignore ├─ front/ // frontディレクトリがGitに管理されない └─ server/
gti cat-file -p でGitのデータ構造を見てみると以下のような結果になった。
$ git cat-file -p HEAD tree 50007e590683eb6c28149a8ce034e8844eaad78d parent f3b00e429aa0ac688d531b7bd6aa4ee2e10a4e6a author ... committer ... $ git cat-file -p 50007e590683eb6c28149a8ce034e8844eaad78d 100644 blob 1761c01d0571f991ca5b544e490766b936946180 .gitignore 160000 commit bb84439791592e7db3491d58eaf19ffce9469371 front 040000 tree 959eaa356045c4f60752dbb6306806509373f993 server
対象のディレクトリがtreeでなく、commitと表示されている。treeオブジェクトがcommitオブジェクトを参照することはありえないはず。また .git/objectsにハッシュ値bb84439791592e7db3491d58eaf19ffce9469371を持つオブジェクトは存在しなかった。
原因
ポジトリが入れ子になってたのが原因。サブディレクトリの.git1は削除してあった。しかし、削除前にそのディレクトリでコミットしたため(サブディレクトリは1つのリポジトリとして完結してるので可能)、ディレクトリではなくサブモジュール扱いになってたらしい。
サブモジュールはgit cat-fiile -pだとcommitとして表示されるようだ。一方でモード情報の表示は160000でGitlinkの番号になってる。Gitlinkはサブモジュールとなっているリポジトリのコミットへの参照であるが、今回の場合、参照先のリポジトリの.gitはすでに削除されているので、対象のコミットは存在していない。
対処方法
Gitlink を削除してadd & commit
$ git rm --cached front $ git commtit -am "delete front gitlink"
その他
同じ状況を再現してやってみると(サブディレクトリで先にコミット)、リポジトリが入れ子になった状態でaddしたときに丁寧な警告が出ていた。
$ ls front server $ git add . warning: adding embedded git repository: front hint: You've added another git repository inside your current repository. hint: Clones of the outer repository will not contain the contents of hint: the embedded repository and will not know how to obtain it. hint: If you meant to add a submodule, use: hint: hint: git submodule add <url> front hint: hint: If you added this path by mistake, you can remove it from the hint: index with: hint: hint: git rm --cached front hint: hint: See "git help submodule" for more information.
参考
VSCodeで React を書くための準備(JavaScript)
方針
VSCodeでReact(JavaScript)を書くための設定を行う。Reduxを使用することを想定している。
ファイル保存時にESLintで構文チェックを行う際に、Prettierのフォーマットも実行する。ESLintで修正できないコーディングスタイルをPrettierでカバーするイメージだが、互いのルールが矛盾した場合はPrettierの方を採用される。ESlintのルールの中で、Prettierと矛盾せずまたESLintの自動フォーマットで修正できなかったものは、エディタ上に警告として表示されるようにする。
手順
必要なパッケージのインストール
以下のコマンドでnpmパッケージをインストール
$ npm install --save-dev eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react-redux babel-eslint
- eslint
- ESLint本体
- prettier
- Prettier本体
- eslint-config-prettier
- Prettierのルールと矛盾するESLint のルールを無効化
- eslint-plugin-prettier
- ESLint内でPrettierの実行
- eslint-plugin-react-redux
- ReduxのためのLintのルール設定
- babel-eslint
- jsxをパースするために使用
VSCode拡張のインストール
VS Code ESLint extensionをインストールする。
VSCodeの設定
settings.jsonに以下の内容を追記する。
"eslint.enable": true, "[javascript]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } }, "[javascriptreact]": { // jsx "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } },
"editor.formatOnSave": false はPrettierのVSCode拡張を使った自動フォーマットを有効化している場合に必要(今回の設定にPrettierのVSCode拡張自体は不要)。
ESLintとPrettierの設定
プロジェクトのディレクトリに .eslintrc.js を作成し、以下の内容を記述する。
module.exports = { env: { es6: true, browser: true, // ブラウザを前提とした予約語(console, document等)への警告を無効化 }, parser: "babel-eslint", parserOptions: { sourceType: "module", // import文の警告を無効化 }, plugins: ["react-redux"], extends: [ "eslint:recommended", "plugin:react-redux/recommended", "plugin:prettier/recommended", ], rules: { "prettier/prettier": [ "error", { // prettierの設定(何も書かなくてもデフォルトの設定は有効) // 別ファイルに.prettierrcとして設定することも可能 singleQuote: true, semi: true, }, ], }, };