Go の struct と interface で Embedding
Effective Go の Embedding の内容を試してみます。
Go では embedding を利用して継承のようなことができますが、struct と interface の違いが今ひとつ理解できていなかったため、実際にコードを書いてコンパイラに怒られながら、どういう違いがあるのか試していきたいと思います。
- 確認方法
- interface を interface に埋め込む
- struct を struct に埋め込む
- struct を interface に埋め込む(不可)
- interface を struct に埋め込む
確認方法
こんな感じで embedding を利用して BaseMethod
と SuperMethod
を実装するパターンを色々見ていきます。
type baseInterface interface { BaseMethod() string } type superInterface interface { baseInterface SuperMethod() string }
実装の確認用兼、interface を満たしているかの確認用の関数を用意します。
func printSuper(name string, v superInterface) { fmt.Println(name+".BaseMethod ->", v.BaseMethod()) fmt.Println(name+".SuperMethod ->", v.SuperMethod()) }
interface を interface に埋め込む
type baseInterface interface { BaseMethod() string } // 一応 baseInterface の方の実装も作っておく type implOfBaseInterface struct{} func (implOfBaseInterface) BaseMethod() string { return "implOfBaseInterface.BaseMethod" } type superInterface interface { baseInterface SuperMethod() string } type implOfSuperInterface struct{} // interface を interface に埋め込んだ場合 BaseMethod の定義は必要 func (i implOfSuperInterface) BaseMethod() string { return "implOfSuperInterface.BaseMethod" } func (implOfSuperInterface) SuperMethod() string { return "implOfSuperInterface.SuperMethod" }
impleOfSuperInterface
側でも BaseMethod
の実装が必要になります。
実装を参照する先も無いので当たり前と言えば当たり前ですね。
func main() { printSuper("implOfSuperInterface", implOfSuperInterface{}) }
してみると
implOfSuperInterface.BaseMethod -> implOfSuperInterface.BaseMethod implOfSuperInterface.SuperMethod -> implOfSuperInterface.SuperMethod
これも impleOfSuperInterface
に実装したものがそのまま呼ばれて、当たり前の結果ですね。
このパターンの埋め込みをするメリットとしては、interface 側のメソッドの定義を省略できることくらいでしょうか。
struct を struct に埋め込む
type baseStruct struct{} func (b baseStruct) BaseMethod() string { return "baseStruct.BaseMethod" } type superStruct struct { baseStruct } // この場合 BaseMethod は省略できるが実装してもいい // func (s superStruct) BaseMethod() string { // // 明示的に baseStruct.BaseMethod を呼ぶこともできるし(あまり意味なさそう) // return s.baseStruct.BaseMethod() // // 他の処理を定義してもいい // return "superStruct.BaseMethod" // } func (s superStruct) SuperMethod() string { return "superStruct.SuperMethod" }
struct を struct に埋め込んだ場合は superStruct.BaseMethod
の実装を省略できるみたいです。
func main() { printSuper("superStruct", superStruct{}) }
してみると
superStruct.BaseMethod -> baseStruct.BaseMethod superStruct.SuperMethod -> superStruct.SuperMethod
superStruct.BaseMethod
で baseStruct.BaseMethod
が呼ばれていますね。
struct を interface に埋め込む(不可)
type structEmbeddedInterface interface { baseStruct SuperMethod() } type implOfStructEmbeddedInterface struct{} func (implOfStructEmbeddedInterface) SuperMethod() { fmt.Println("structEmbeddedInterface.SuperMethod") }
これは不可能なパターンで、コンパイル時に interface contains embedded non-interface baseStruct
と怒られます。
interface を struct に埋め込む
type interfaceEmbeddedStruct struct { baseInterface } func (interfaceEmbeddedStruct) SuperMethod() string { return "interfaceEmbeddedStruct.SuperMethod" }
この場合は interfaceEmbeddedStruct.BaseMethod
の実装をしなくてもコンパイルは通ります。しかし、ランタイムで panic: runtime error: invalid memory address or nil pointer dereference
というエラーになります。一番タチが悪いですね。
このパターンの活用事例は悩ましいですが、考察されているページがあったので時間のある時に読みたいと思います。 https://horizoon.jp/post/2019/03/16/go_embedded_interface/
今回触って改めて、このGo言語における embedding は手を動かさないと理解できないなと思いました。単純にパターンが多いので網羅しづらいのと、網羅されても今度は長大になるため読み解く時間がないので、手元で試した方が速いなと…
dataloader を使ってリクエストを取りまとめる React デモを作る
以前 GraphQL を利用していたときに、サーバサイドのSQLリクエストを減らすために使おうとしていたパッケージに dataloader というものがあります。
現在は GraphQL ではなく gRPC-Web を利用したアプリを書いているのですが、dataloader
は別に GraphQL に限ったものではなく、id 単体を指定するような複数のリクエストを、複数の id
を指定する単一のリクエストにまとめて実行するということをやってくれます*1。つまり dataloader
は
get****ById(id1)
x n → get****ByIds(id1, id2, ...)
という処理を行ってくれる中間層として機能してくれるものになります。
デフォルトではレスポンスに含まれる id
を利用して、リクエストの取りまとめや重複するキーへのリクエストの dedupe 処理を行ってくれます。
GraphQL のサーバサイドではスキーマの patching を行っていたのですが、GraphQL はレスポンスに id
を含めるかどうかが叩く側次第なためこの辺りの処理ががうまくいかずに頓挫しました。
話を戻して、今回は GraphQL ではなく、一般化したリモートリクエストをWebから行うようなAPIクライアントを対象にして、dataloader
の動作を以下のような単純なアプリを作って試していきたいと思います。
動作の詳しい仕組みは理解していませんが、とりあえず使ってみるところから始めます。
この記事で説明しているコードの、実際に動作するプロジェクトは以下の til リポジトリに置いてあります。
下準備
API クライアントのモックを定義する
今回デモで利用するAPIは以下のようなものです。
// api.ts // Data Types // ---------- export type IdType = number export interface User { id: IdType name: string favoriteThingIds: IdType[] } export interface Thing { id: IdType name: string } // Data Mock // --------- const thingList: Thing[] = [ { id: 1, name: "Raindrops" }, { id: 2, name: "Kittens" }, { id: 3, name: "Kettles" }, { id: 4, name: "Mittens" }, { id: 5, name: "Packages" } ] const userList: User[] = [ { id: 1, name: "Alice", favoriteThingIds: [1, 2, 3, 4, 5] }, { id: 2, name: "Bob", favoriteThingIds: [2, 4] }, { id: 3, name: "Carol", favoriteThingIds: [1, 3] } ] // API Mock // -------- const emulateNetworkDelay = <V>(value: V): Promise<V> => { return new Promise(resolve => setTimeout(() => resolve(value), 1000)) } export default { getUserById(id: IdType) { console.log(`API called: getUserById(${id})`) const user = userList.find(u => u.id === id) return emulateNetworkDelay(user) }, getUsersByIds(ids: IdType[]) { console.log(`API called: getUsersByIds(${ids})`) const users = ids.map(id => userList.find(u => u.id === id)) return emulateNetworkDelay(users) }, getThingById(id: IdType) { console.log(`API called: getThingById(${id})`) const thing = thingList.find(t => t.id === id) return emulateNetworkDelay(thing) }, getThingsByIds(ids: IdType[]) { console.log(`API called: getThingsByIds(${ids})`) const things = ids.map(id => thingList.find(t => t.id === id)) return emulateNetworkDelay(things) } }
データ型としては User
が Thing
への参照を id
のみで持っており、Thing
の内容を見たい場合は更にAPIを叩く必要がある、というAPI設計です。
ByIds
のクエリにより id
複数指定でデータを取得することができるという想定です。dataloader
の利用にはこういったAPIが存在することが前提となります。
しかしこの ByIds
系のクエリですが、実際に React アプリを書いた経験から言うと、意外と活かしづらいです。私が React を使う場合、コンポーネントがマウントされたタイミングでそのコンポーネントが必要とするリクエストを行う、という方針でコンポーネントを切り分け・実装することが多いです。なのでリクエストの効率化のために ByIds
でデータ取得の取りまとめを行おうとすると、ちょっと面倒くさく感じる場面が多いですね。
この辺の処理を React のデータフローだけで組もうとすると、実際にデータが必要なコンポーネントのより親階層の方で子コンポーネントに必要なリクエストを取りまとめて行う必要がありそうですが、それをやろうとすると描画コンポーネントと、データを取得するコンテナコンポーネントの間の仕様的な結合が強くなり、使い回しをしづらいコンポーネントになってしまうのではないかと思っています。
という事情もあり ByIds
的なAPI側が用意されているにも関わらず、実際には React での組みやすさによる制約が働いて「複数 id
指定使ってないです、すみません...」となることが今までよくありました。
APIクライアントに対して dataloader を噛ませたラッパーを用意する
さて、前置きが長くなりましたが、上記のAPIクライアントに対して、以下のような dataloader
を定義してAPIクライアントのラッパーを定義します。
// api-with-dataloader.ts import DataLoader from "dataloader" import api, { IdType } from "./api" // DataLoaders // ----------- const userLoader = new DataLoader((keys: IdType[]) => api.getUsersByIds(keys)) const thingLoader = new DataLoader((keys: IdType[]) => api.getThingsByIds(keys)) // Batching API Mock // ----------------- export default { getUserById(id: IdType) { return userLoader.load(id) }, getThingById(id: IdType) { return thingLoader.load(id) } }
User
と Thing
のそれぞれに dataloader
を仕込んだAPIクライアントのラッパーを定義します。ByIds
分の定義も loadMany
を利用すればできそうですが、今回のデモでは利用しないので省略します。
React コンポーネントを定義する
これらのAPIクライアントを利用するコンポーネントを以下のように組みます。
import React, { Component, useEffect, useState } from "react" import logo from "./logo.svg" import "./App.css" import api, { IdType, User, Thing } from "./api" import batchApi from "./api-with-dataloader" // ThingSummary // ------------ interface ThingSummaryProps { id: IdType batch?: boolean } function ThingSummary(props: ThingSummaryProps) { const { id, batch } = props const [thing, setThing] = useState<Thing | undefined>(undefined) useEffect(() => { if (batch) { batchApi.getThingById(id).then(setThing) } else { api.getThingById(id).then(setThing) } }, []) if (!thing) return <div>loading...</div> return ( <div> #{thing.id}: {thing.name} {batch && " (batched!)"} </div> ) } // UserSummary // ----------- interface UserSummaryProps { id: IdType batch?: boolean } function UserSummary(props: UserSummaryProps) { const { id, batch } = props const [user, setUser] = useState<User | undefined>(undefined) useEffect(() => { if (batch) { batchApi.getUserById(id).then(setUser) } else { api.getUserById(id).then(setUser) } }, []) if (!user) return <div>loading...</div> return ( <div> <h3> #{user.id}: {user.name}'s Favorite Things {batch && " (batched!)"} </h3> <ul> {user.favoriteThingIds.map(id => ( <ThingSummary key={id} id={id} batch={batch} /> ))} </ul> </div> ) } // Main Component // -------------- export default class App extends Component { render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <div> <UserSummary id={1} batch /> <UserSummary id={2} batch /> <UserSummary id={3} /> </div> </header> </div> ) } }
User
と Thing
それぞれに Summary
コンポーネントを定義して、それぞれのコンポーネントは必要なデータを自身がマウントされた際にリクエストを飛ばして取得する、という設計です。こういう処理が簡単に書けるので React Hook 便利ですね。
それぞれの Summary
コンポーネントには batch
オプションをプロパティとして定義しています。有効化された場合には素の api
の代わりに dataloader
を仕込んだ batchApi
が利用されます。batch
が指定された UserSummary
は自身のリクエストの他に子コンポーネントの ThingSummary
の batch
オプションも有効化するようにしています。
全体としては、id
が 1
と 2
のユーザ対しては batch
リクエストとして dataloader
を利用したAPIクライアントの方を利用し、3
のユーザに対しては元のAPIクライアントを利用する、という描画内容になっています。
実行する
実行結果をイメージするために、改めてデータの中身と描画部分のコードを載せておきます。
const thingList: Thing[] = [ { id: 1, name: "Raindrops" }, { id: 2, name: "Kittens" }, { id: 3, name: "Kettles" }, { id: 4, name: "Mittens" }, { id: 5, name: "Packages" } ] const userList: User[] = [ { id: 1, name: "Alice", favoriteThingIds: [1, 2, 3, 4, 5] }, { id: 2, name: "Bob", favoriteThingIds: [2, 4] }, { id: 3, name: "Carol", favoriteThingIds: [1, 3] } ]
<UserSummary id={1} batch /> <UserSummary id={2} batch /> <UserSummary id={3} />
もしこれらのデータが全て ById
によって取得された場合、叩かれるAPIの回数としては以下のようになりますね。
実際にアプリを実行してみると冒頭に貼った画像のように、以下のように描画されます。
「batched!」と描画されている箇所が batch
オプションを指定した箇所です。
問題なくデータは指定した id
の通りに取得されているようですね。
コンソールに出力されているのは dataloader
をかませていない、素のAPIクライアントの各メソッドが呼ばれたログです。
コンソールの行数だけ見ても、明らかにリクエスト数は減っていますね。実際、APIクライアントが叩かれた回数としては
getUserById
: 1 リクエスト( id:3
)getUsersByIds
: 1 リクエスト(id:1, 2
)- 直接コンポーネントからは呼んでいませんが
dataloader
経由で呼ばれていますね
- 直接コンポーネントからは呼んでいませんが
getThingById
: 2 リクエスト(id:1, 3
)getThingsByIds
: 1 リクエスト(id:1, 2, 3, 4, 5
)- こちらも同様です
となりました。確かに ById
リクエストが ByIds
リクエストにまとめられていることがわかります。
実際に動作することがつかめたので、使い慣れたあたりで、次は dataloader
の動作原理にも踏み込んでみたいですね。
今回は以上です。
*1:まとめられる単位はソースを軽く眺めたところ実行コンテキスト単位(?)な雰囲気を感じましたが、詳しくはまだ理解できていません
Rubyist じゃないけど Vagrantifile を読みたい
(=˘ ꒳ ˘=) kubernetes を試そうと Vagrant のリハビリしようとしたら Ruby の表記を思い出すのに手間取ったの巻...
Multi-Machine - Vagrant by HashiCorp の Vagrantfile
を例に、初見で読み方に困ったところを思い出しながら書き留めておきます。
Vagrant.configure("2") do |config| config.vm.provision "shell", inline: "echo Hello" config.vm.define "web" do |web| web.vm.box = "apache" end config.vm.define "db" do |db| db.vm.box = "mysql" end end
ブロック(do..end)
Vagrant.configure("2") do |config| # ... end
は以下の表現と等価ですではありませんでした。
Vagrant.configure("2") { |config| # ... }
どちらの表記も見かけるので迷いますね。複数行の場合は do..end
を使う...とかでしょうか。
引数のカッコを省略
config.vm.provision "shell", inline: "echo Hello"
は以下の表現と等価です。
config.vm.provision("shell", { inline: "echo Hello" })
他の言語から来ると悪魔のようにも見える表記ですが、慣れるとだんだん括弧が見えてくるようになりますね。 この表記のおかげで、私の中では「Rubyは設定ファイル言語」という先入観があります。
DSL-like 表記の面目躍如といった所ですね。
参考
Bundler の bundler/setup と bundle exec
(=˘ ꒳ ˘=) Ruby のコードちんぷんかんぷんで読みづらい... import
やら require
やら load
やら色々読み込みの構文があるのに加えて、どのライブラリから来た定義なのかが追いづらかったり...
...という愚痴は置いておいて、Ruby のモジュールシステムを別の方面からややこしくしている Bundler の振る舞いについて整理したいと思います。ややこしいと書きましたが、プロジェクト下に依存ライブラリを置けるという、Node.js から来た身からすると勝手知ったるベンダリング手法が取れるありがたいツールです。
Bundler がインストールしたパッケージを使うためには bundle exec
したり require bundler/setup
したりと、いくつかお作法が必要そうなのですが、それぞれどう作用するかを実際に動かしながら整理していきます。
公式の記述は How to use Bundler with Ruby にあります。しかし情報がパラパラしていて追いづらかったので実際に試してみることにしたのでした。
試してみる
├── Gemfile ├── bin │ ├── with-bundler-setup.rb │ └── without-bundler-setup.rb ├── lib │ └── using-bundled-packages.rb └── vendor
というプロジェクトを用意します。 https://github.com/asa-taka/try-module-on-bundler に実際のファイルを置いておきました。
フロントのスクリプトをふた通り用意します。
# bin/with-bundler-setup.rb require 'rubygems' require 'bundler/setup' require './lib/using-bundled-packages'
# bin/without-bundler-setup.rb require './lib/using-bundled-packages'
孫 require
の振る舞いを見たかったので一段 lib/using-bundled-packages.rb
というスクリプトを挟んでいます。
# lib/using-bundled-packages.rb require 'awesome_print'
として awesome_print
を呼んでいます。vendor 下にインストールされた awesome_print
には print 'awesome_print by bundler loaded'
と仕込んで、Bundler によりインストールされたパッケージが実行された場合にテキストがプリントされるようにしました。
試した結果
bundle exec ruby ./bin/with-bundler-setup
のように各組み合わせを試したところ以下のような結果になりました。
実行コマンド | bundler/setup なし | bundler/setup あり |
---|---|---|
ruby .... |
global のパッケージ | Bundler によるパッケージ |
bundle exec ruby ... |
Bundler によるパッケージ | Bundler によるパッケージ |
つまり bundle exec
を使う、もしくはエントリポイントで require bundler/setup
をすると孫 require
でも Bundler によるパッケージが使われることになりますね。
毎回 bundle exec
するのも面倒なので require bundler/setup
の方が楽そうですね。
どういう処理でこうなっているのかはまた掘り下げてみたいところです。
GraphQL Schema Language 中の description がコメントから block string に変わった
graphql-tools の makeExecutableSchema
のコードを読んでいて commentDescription
なる項目が気になり、さらに
utilities/buildASTSchema.js
を見ていて
export type BuildSchemaOptions = { ...GraphQLSchemaValidationOptions, /** * Descriptions are defined as preceding string literals, however an older * experimental version of the SDL supported preceding comments as * descriptions. Set to true to enable this deprecated behavior. * * Default: false */ commentDescriptions?: boolean, };
と、description の書き方が変わったらしいことに気がつきました。
https://github.com/graphql/graphql-js/issues/1245 によると graphql@0.12.0 から GraphQL Schema Language 中の description の書き方がコメントから block string というものになっていたらしいです。
簡単に GraphQLSchema
を printSchema
して確認してみました。
import { GraphQLSchema, GraphQLObjectType, GraphQLString, printSchema } from 'graphql' const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'rootQuery', description: 'This is Root Query', fields: { hello: { type: GraphQLString, description: 'Greeting', }, }, }), })
というスキーマを用意して
console.log(printSchema(schema))
とすると
schema { query: rootQuery } """This is Root Query""" type rootQuery { """Greeting""" hello: String }
という """
で囲まれた block string による description が得られます。
今まで通りのコメントによる description を得るには commentDescription
という後方互換性のためのオプションが用意されているので
console.log(printSchema(schema, { commentDescriptions: true }))
とすると
schema { query: rootQuery } # This is Root Query type rootQuery { # Greeting hello: String }
と今までどおりの形式で得られます。
graphql-tools の makeExecutableSchema で Directive を定義して簡易認可を実装してみる
Schema directives | GraphQL Tools によると
graphql-tools の makeExecutableSchema
で Directive を実装できるようなので
簡易的な認可ロジックを実装してみます。
あくまで Directive のハンズオンなので認可ロジックはとても簡単なものです。
認証情報は context
経由で処理の部分に渡すことにします。
「タイトルは自由に取得できるけど本文は許された人にしか閲覧できないドキュメント」を取得するためのクエリを定義していきます。
import { graphql } from 'graphql' import { makeExecutableSchema } from 'graphql-tools' const typeDefs = ` directive @auth(permitted: [String!]) on FIELD type Document { title: String! content: String! @auth(permitted: ["asa-taka"]) } type Query { document: Document! } ` // Directive の処理を定義する const directiveResolvers = { // @auth Directive の処理を定義 auth(next, src, args, ctx, info) { return next().then(res => { // コンテクスの認証ユーザが `permitted` に含まれていたら値を素通し if (args.permitted.includes(ctx.auth.name)) return res // 認証エラーを返してあげる (*˘꒳˘*) やさしい // info の中身は複雑なので適当に調べながら... const path = info.path throw new Error(`User not permitted: ${path.prev.key}.${path.key}`) }) }, } const schema = makeExecutableSchema({ typeDefs, directiveResolvers }) const query = `{ document { title content } }` // データソースの準備 const rootValue = { document: { title: 'My Document', content: 'Awesome contents...' }, } // コンテクスト経由で認証情報を渡してやる const context = { auth: { name: 'asa-taka' } } graphql(schema, query, rootValue, context).then(console.log, console.error)
これを実行すると
{ data: { document: { title: 'My Document', content: 'Awesome contents...' } } }
が得られます。ここから認証情報を
const context = { auth: { name: 'some-other-user' } }
と変更すると
{ errors: [ { Error: User not permitted: document.content
とエラーが返されます。ここから更に認証フィールドである content
をクエリから除外して
const query = `{ document { title } }`
とすると
{ data: { document: { title: 'My Document' } } }
エラーなくデータが得られます。
ディレクティブの定義
基本的な使い方は Schema directives | GraphQL Tools に書いてある通りです。
まず GraphQLSchema
内で利用する Directive を定義します。
GraphQL Schema Language では以下のように Custom Directive を定義できます
directive @auth(permitted: [String!]) on FIELD
次に定義した Directive についての処理の実装を定義します。
graphql-tools の makeExecutableSchema
では directiveResolver
プロパティでディレクティブの処理を定義できます。
const directiveResolvers = { auth(next, src, args, ctx, info) { return next().then(res => { if (args.permitted.includes(ctx.auth.name)) return res const path = info.path throw new Error(`User not permitted: ${path.prev.key}.${path.key}`) }) }, } const schema = makeExecutableSchema({ typeDefs, directiveResolvers })
GraphQL の Directive も Apollo のおかげで意外と簡単に実装できますね。
これまで
...と、Directive 実装に苦労してきた経緯があるのですが、makeExecutableSchema
はどう実装されているのか気になりますね。
GraphQL Schema Language で Directive を定義する
(=˘ ꒳ ˘=) GraphQL Schema Language 内で Directive を定義する方法を探していたのですが、公式にドキュメントが見つからなかったのでメモしておきます...
directive @myDirective(age: Int) on FIELD
のように Directive を定義できるようです。
import { graphql, buildSchema } from 'graphql' const schema = buildSchema(` directive @myDirective(age: Int) on FIELD type Query { hello: String! } `) const query = `{ hello @myDirective(age: 12) }` const rootValue = { hello: 'world' } graphql(schema, query, rootValue).then(console.log, console.error)
のように実行できます。
見つけた背景
GraphQLDirective
を使って定義した後にそれを printSchema
するとどうなるか興味本位で試していて見つけました。
import { graphql, GraphQLSchema, GraphQLDirective, GraphQLObjectType, GraphQLString, printSchema, } from 'graphql' const myDirective = new GraphQLDirective({ name: 'myDirective', locations: ['FIELD'], args: { age: { type: GraphQLString }, }, }) const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'rootQuery', fields: { hello: { type: GraphQLString }, }, }), directives: [myDirective], }) console.log(printSchema(schema))
これを実行すると
schema { query: rootQuery } directive @myDirective(age: String) on FIELD type rootQuery { hello: String }
と表示されます。Directives はてっきり GraphQL Schema Language の対象外かと思っていたのですが定義できたのですね。
私が見つけられていないだけで、公式の記述はどこかにあるのでしょうか。
そして定義ができることと処理を実装できることはまた別なのですよね... Directive の処理をいい感じに定義する方法はあるのでしょうか...