結合形式


カバー画像:ランタン、アナShere )
最近私は学んでいるElm そして、私は完全にそのunion types . この記事では、JavaScriptのユニフォーム型を実装する方法を示します.

ユニオン型とは
ユニオンタイプは、また、代数的データ型(またはADT)として知られている、複数のフォームを取ることができる複雑なデータを表現する方法です.ユニオンタイプ理論に深く潜んではいけないWikipedia article それらを説明するのに優れた仕事です.
あなたが今のところ知っている必要があるすべては、ユニオンタイプは、私たちが複数のフォームを取ることができるデータを表現し、分類することができるタイプです、enumのように、しかしより強力です.

ユニオンタイプを実装する方法
ユニオンタイプがなぜ役に立つか、そして、それらを使用するかを調べる前に、JavaScriptでそれらを実装しようとしましょう.ここでは、私が呼ぶヘルパー機能を実装しましたunion . 型名のリストを受け取り、union型を表すオブジェクトを返します.
const union = types =>
    types.reduce((prev, type) => ({
        ...prev,
        [type]: data => ({
            match: fns => fns[type](data),
        }),
    }), {})
あなたがよく知らないならばreduce Works、あなたは見なければなりません、しかし、ここでは、forループを使用する大まかに同等のバージョンです.
const union = types => {
    const result = {}
    for (let type of types) {
        result[type] = data => ({
            match: fns => fns[type](data),
        })
    }
    return result
}
この関数は、types 配列.各タイプは、いくつかのデータを受け取り、メソッドを持つオブジェクトを返すことができるファクトリですmatch . 方法match は各オブジェクトの関数を持つオブジェクトを受け取り、オブジェクトが属する特定の型の関数を実行します.
今、我々はunion ユニオンタイプを作成するヘルパー.
これが愚かな例でどう動くかを説明しましょう.私たちはポニーについてのデータを処理する必要があることを想像してください.誰もが知っているように、ポニーの3種類があります:地球ポニー、ペガシとユニコーン.それぞれのタイプの特定の能力を特定の種類があります.例えば、ペガシは飛ぶことができます、そして、ユニコーンは魔法を使うことができます.
const Ponies = union([
    'EarthPony',
    'Pegasus',
    'Unicorn',
])

const twilight = Ponies.Unicorn({
    name: 'Twilight Sparkle',
    spell: 'Levitation',
})
const rainbow = Ponies.Pegasus({
    name: 'Rainbow Dash',
    speed: 20,
})

twilight.match({
    EarthPony: ({ name }) => `${name} is a peaceful earth pony.`,
    Pegasus: ({ name, speed }) => `${name} flies at a speed of ${speed}!`,
    Unicorn: ({ name, spell }) => `${name} uses ${spell}!`,
}) // -> 'Twilight Sparkle uses Levitation!'
我々は、メソッドを使用することができますmatch 我々が持っているポニーの種類に応じて特定のロジックを実行します.我々がどのように使うかに似ているswitch に関する声明enum Javaでは、それぞれの型が異なる型データを関連付けることができます.

使用例
組合のタイプが本当のアプリケーションで使われることができる方法の考えを得るために、半月の半月のない愚かな例を見ましょう.

例1 :ノードのエラーを処理する
ノードとExpressを使用してREST APIを構築しているふりをしましょう.js私たちのAPIはIDによってデータベースからポニーを返す終点を持っています.
私たちのエクスプレスアプリはこのように見える.
const mongodb = require('mongodb')
const express = require('express')

const app = express()

mongodb.MongoClient.connect(DB_URL)
    .then(client => client.db(DB_NAME))
    .then(db => {
        app.get('/ponies/:id', /* here be our endpoint */)
        app.listen(3000, () => 'Server started.')
    })
あなたが急行に慣れていないならば、心配しないでください.あなたが知る必要があるのは、リクエストオブジェクトを受け取る関数を実装することですreq ) レスポンスオブジェクト(res ) また、この関数はデータベース接続へのアクセスをdb .
我々のPonyデータベースが非常に敏感な情報を保持するので、我々の機能はユーザーが認証されるのをチェックします.そして、id パスからパラメータを受け取り、そのIDをデータベースから取得します.最後に、応答にポニーデータを送信します.
間違って行くことができる少なくとも3つのものがあります.
  • ユーザセッションは期限切れになっているかもしれません、あるいは、ユーザは有効なトークンなしでAPIにアクセスしようとしているかもしれません.
  • 指定されたIDを持つデータベースにポニーがないかもしれません.
  • 予期せぬ失敗があるかもしれない.例えば、データベースはダウンしているかもしれません.
  • これら3種類のエラーをモデル化するユニオンタイプを作りましょう.
    const ApiError = union([
        'InvalidCredentials',
        'NotFound',
        'Other',
    ])
    
    ユーザーが適切に認証されていない場合は、InvalidCredentials エラーです.ポニーがデータベースに存在しないならば、我々はAを返しますNotFound . すべての予期しないエラーをグループ化しますOther .
    最初のステップを見てみましょう.という関数を仮定しましょうauthorise これはユーザトークンをチェックし、true それが有効であるならばfalse そうでなければ、ヘッダーやクッキーからユーザトークンを読み込み、それを保存するミドルウェアを持っていますreq.bearer . 我々は、呼び出しをラップしますauthorise 我々はいくつかの非同期操作を持っているので、約束の拒絶枝を通してすべてのエラーを処理したいので、約束で.
    app.get('/ponies/:id', (req, res) =>
        new Promise((resolve, reject) => {
            if (authorise(req.bearer)) return resolve()
            return reject(ApiError.InvalidCredentials())
        })
    )
    
    これまでのところ良い.ユーザが適切に認証されないなら、約束は拒絶されます、そして、我々はチェーンの残りを実行しません.さもないと、データベースからポニーを読むことができます.私たちは別の約束でデータベースに呼び出しをラップして、データベースで見つけるならば、データでそれを解決しますNotFound エラーです.
    app.get('/ponies/:id', (req, res) =>
        new Promise((resolve, reject) => {
            if (authorise(req.bearer)) return resolve()
            return reject(ApiError.InvalidCredentials())
        })
        .then(() => new Promise((resolve, reject)) =>
            db.collection('ponies').findOne({ id: req.params.id }, (err, data) => {
                if (err) {
                    return reject(ApiError.Other(err))
                }
                if (data == null) {
                    return reject(ApiError.NotFound(`Pony ${req.params.id} not found.`))
                }
                return resolve(data)
            })
        )
    )
    
    何かが間違っているなら、ノードcallbackはエラーを返すことができますerr , 我々は、約束を拒否しますOther エラーです.操作が成功した場合は、データベースにレコードがなかった場合にはデータを返しません.NotFound エラーです.さもなければ、我々には若干のデータがあります、そして、我々はそれで約束を解決することができます.
    すべてがうまくいったなら、次のステップは応答にデータを送り返すことです、さもなければ、我々は間違ったことに応じてHTTPエラーを送りたいです.
    app.get('/ponies/:id', (req, res) =>
        new Promise((resolve, reject) => {
            if (authorise(req.bearer)) return resolve()
            return reject(ApiError.InvalidCredentials())
        })
        .then(() => new Promise((resolve, reject)) =>
            db.collection('ponies').findOne({ id: req.params.id }, (err, pony) => {
                if (err) {
                    return reject(ApiError.Other(err))
                }
                if (pony == null) {
                    return reject(ApiError.NotFound(`Pony ${req.params.id} not found.`))
                }
                return resolve(pony)
            })
        )
        .then(pony => res.json(pony))
        .catch(err => err.match({
            InvalidCredentials: () => res.sendStatus(401),
            NotFound: message => res.status(404).send(message),
            Other: e => res.status(500).send(e)
        }))
    )
    
    そしてそれです.もし拒絶枝にエラーがあれば、このメソッドを使うことができますmatch 関連するHTTPステータスコードと異なるメッセージを送るには.
    我々が正直であるならば、これは非常に印象的でありません.我々は、enumのようなオブジェクトと全く同じことをすることができました.タイプマッチングはかなり優雅だと思いますが、良いolに比べて大きな違いはありませんswitch 文.
    この例で完全な例を確認できますGitHub repo .

    例2 :反応コンポーネントでリモートデータを取得する
    では、別の例を試してみましょう.リモートサーバーからいくつかのデータをロードする反応コンポーネントがあると偽ります.あなたがそれについて考えるならば、この構成要素は4つの州のうちの1つを持つことができました:

  • 尋ねない.データはまだサーバーに問い合わせられていません.

  • 未定.データはサーバーに尋ねられましたが、応答はまだ受けませんでした.

  • 成功データはサーバから受信されました.

  • 失敗.通信中にエラーが発生しました.
  • ユニオンタイプでこれをモデル化しましょう.
    const RemoteData = union([
        'NotAsked',
        'Pending',
        'Success',
        'Failure',
    ])
    
    そこに行く.今、我々は状態でロードされる反応コンポーネントを作成したいNotAsked と状態に応じて別のものをレンダリングします.
    class Pony extends React.Component {
        constructor(props) {
            super(props)
            this.state = {
                data: RemoteData.NotAsked()
            }
        }
    }
    
    いくつかのデータを保持し、状態から始まるコンポーネントを作成しましたNotAsked . その状態を描きましょう.サーバーに呼び出しを引き起こすために、データとボタンをロードするようにユーザーに伝えているテキストが欲しいでしょう.
    class Pony extends React.Component {
        // previous code here...
        render() {
            return this.state.data.match({
                NotAsked: () => (
                    <div>
                        <h1>Press "load"</h1>
                        <button onClick={this.fetchData}>Load!</button>
                    </div>
                )
            })
        }
    }
    
    あなたは気づいたかもしれないonClick={this.fetchData}button . ユーザーがボタンを押すと、サーバーに要求をトリガーしたいので、Aを追加する必要がありますfetchData コンポーネントへのメソッド.しかし、最初に、私たちが呼び出しに実際のサーバーを持っていないので、サーバーへの呼び出しをシミュレーションする関数を作成しましょう.
    const fetchPony = () => new Promise((resolve, reject) =>
        setTimeout(() => {
            if (Math.random() > 0.2) {
                return resolve({
                    name: 'Twilight Sparkle',
                    type: 'Unicorn',
                    element: 'Magic',
                })
            }
            return reject({
                message: `I just don't know what went wrong.`,
            })
        },
        500)
    )
    
    機能fetchPony 500 milesecondsで解決する見込みを返します、サーバーへの往復をシミュレートして、私たちに状態変化を見る若干の時間を与えてください.また、時間の20 %のエラーを返すので、その状態も見ることができます.
    さあ、実行しましょうfetchData メソッドPony コンポーネント.
    class Pony extends React.Component {
        constructor(props) {
            // previous code here...
            this.fetchData = this.fetchData.bind(this)
        }
    
        fetchData() {
            this.setState({ data: RemoteData.Pending() })
            fetchPony()
                .then(pony => this.setState({ data: RemoteData.Success(pony) }))
                .catch(err => this.setState({ data: RemoteData.Failure(err) }))
        }
    
        // render method here...
    }
    
    方法fetchData まず第一に、国家をPending , そしてサーバへの呼び出しをシミュレートします.約束が解決すると、それは状態を変更しますSuccess を返します.エラーが発生した場合、Failure を返します.
    最後のステップは、3つの欠けた州をレンダリングすることです.
    class Pony extends React.Component {
        // previous code here...
    
        render() {
            this.state.data.match({
                NotAsked: () => (
                    <div>
                        <h1>Press "load"</h1>
                        <button onClick={this.fetchData}>Load!</button>
                    </div>
                ),
                Pending: () => (
                    <div>
                        <h1>Loading...</h1>
                    </div>
                ),
                Success: ({ name, type, element }) => (
                    <div>
                        <p><strong>Name:</strong> {name}</p>
                        <p><strong>Type:</strong> {type}</p>
                        <p><strong>Element of Harmony:</strong> {element}</p>
                        <button onClick={this.fetchData}>Reload</button>
                    </div>
                ),
                Failure: ({ message }) => (
                    <div>
                        <p>{message}</p>
                        <button onClick={this.fetchData}>Retry</button>
                    </div>
                )
            })
        }
    }
    
    そして、我々はしました!私たちには、何が起こっているかについて、ユーザーに通知するコンポーネントがあります.
    この例で完全な例を確認できますGitHub repo .

    この実装の制限
    この実装をelmのunion型と比較すると、それはむしろ欠陥を見つけるでしょう.ELMは強く型付けされた言語です、そして、我々がユニオンタイプの枝を扱うのを忘れたか、我々が間違ったタイプのデータに合っているならば、Complierは我々に話します.また、データの特異性が変化する限り、エルムは1つのタイプを複数回マッチさせることができます.JavaScriptを使用すると、このようなことはありません.
    真実は、この実装では、我々のコードエディタから任意のautocompleteのヘルプを持っていないと言われる.しかし、それはより詳細な実装、またはtypescriptタイピングを使用して対処することができます.

    結論
    この記事では、どのようにユニオン型をJavaScriptで実装することができるのかを調べたいと思います.私は、これについての複雑な感情があると言わなければなりません.私はパターンが好きです、そして、私はそれが理由を説明して、広げるのが簡単であるコードを生産することに成功すると思います.一方で、我々は静的に入力された言語から得られるすべての安全性を逃します.そして、我々は本当に我々のコードのちょうどいくつかの賢明な構造でできなかった何かを成し遂げませんでした.
    あなたはどう思いますか.ユニオン型は機能的プログラミングの審美的選好に訴えることを超えて有用であるか?コメント欄であなたの考えや意見を読みたいです.