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)

参考

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を使用したコードを、フックによってリファクタリングする。

フックの導入 – React

今回はステートフック(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.

参考


  1. frontディレクトリはcreate-react-appで生成したReactのテンプレートのため自動で.gitが生成された。

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,
      },
    ],
  },
};

参考