これは、 FORCIA Advent Calendar 2021 の16日目の記事です。 はじめに 新卒1年目の井上と申します。本格的な業務を開始して以来、TypeScriptという言語を触ってきました。TypeScriptというのはその名の通り、JavaScriptに型を付けたような言語です。学生のころよく書いていた言語といえばC++やJavaなのですが 1 、どうやらそれらの言語よりも型でいろんなことができるようで、せっかくなのでTypeScriptの豊かな機能を学んでみたいとなんとなく思っていました。 そんなことを考えていたら、 type-challanges というサイトの存在を教えてもらいました。このサイトでは、与えられた型から新たな型を生み出すという問題を解きながら、TypeScriptの型について学ぶことができるようです。何かしらの問題を解くのが好きな筆者にとってはうってつけの場所です。本記事では、type-challangesに掲載されている、主に文字列に関する問題を解きながら 2 、TypeScriptの型でできることを紹介していきたいと思います。実務に何か生かすというよりは、こういう遊びもあるんだなという気持ちで読んでいただければ幸いです。 TypeScriptの型機能の紹介 問題を解くには知識が必要です。筆者が持つ(または参照できる)すべての知識 3 の解説をするのはここでは控えますが 4 、本記事で扱う問題を解く上でキーとなる機能について少し紹介しましょう。 リテラル型 TypeScriptの基本的な型(プリミティブ型)としては、 number 型、 string 型、 boolean 型などが挙げられますが、それらをさらに細分化した型がリテラル型です。 const age = 25; //ageは25型 const name = "inoue"; //nameは"inoue"型 const yes = true; //yesはtrue型 let age = 25; //ageはnumber型 ここで const で宣言した変数の型はそれぞれ number 、 string 、 boolean ではない ことに注意してください。例えば "inoue" 型というのは、 "inoue" という文字列しか代入できない型となります。 const で宣言された場合は再代入を許さないので、型としてもそれで十分だということですね。 let で宣言した場合は、別の値が入ることがあるのでプリミティブ型となります。 リテラル型が登場するシーンは他にもあります。 const printHello = (str : "World") => { console.log(`Hello ${str}!`); }; printHello("World"); printHello("Japan"); // compile error printHello という関数は、引数として " World " 型しか許さない関数として定義されます。これだと制約がきつすぎますが、 Union型 5 というものを考えることで、柔軟な型付けが可能となります。 type Country = "Japan" | "America"; const printHello = (country : Country) => { console.log(`Hello ${country}!`); }; printHello("Japan"); printHello("America"); printHello("Tokyo"); // compile error 型 A と B に対して型 C = A | B は、 A 型か B 型であるような型を表します。上記の場合、 printHello という関数は、引数として "Japan" 型か "America" 型しか許さない関数として定義されます。こうしてみると結構使いどころがありそうですね。 代入可能 文字列のリテラル型 "World" 型ですが、これを string 型として扱いたいというときもあるかもしれないし、実際そのようにみなすこともできます。このことを、ここでは " W orld " 型は string 型に 代入可能である ということにしましょう。このような関係は string 型だけではなく number 型、 boolean 型にも存在しますし、 "Japan" 型や "America" 型は "Japan" | "America" 型に代入可能です。型 A が型 B に代入可能なことを、 A extends B と書いたりします。 型から新しい型を作る TypeScriptでは、ジェネリクスを用いることで、型から新しい型を作る関数のようなものを作ることができます。 type getUnion<T, U> = T | U; type ex0 = getUnion<string, number>; //ex0はstring | number 型 type ex1 = getUnion<"a", "b">; //ex1は"a" | "b" 型 getUnion は、型 T と型 U を受け取り、型 T | U を返す関数です。このように、型から新しい型を作る関数のことをここでは型関数と呼ぶことにしましょう。 これは単純な例ですが、後に見るようにさまざまな型を生成できます。 ここからは少し高度な型の機能について見ていきます。 Conditional Types Conditional Typesの構文は以下のようになります。 T extends U? A : B この構文は、「 T が U に代入可能であれば、 A 型、そうでなければ B 型を返す」という意味です。三項演算子みたいなものですね。 type ex0 = "a" extends string? "x" : "y"; // "x"型 type ex1 = 0 extends string? "x" : "y"; // "y"型 type ex2 = "a" extends "a" | "b"? "x" : "y"; // "x"型 type ex3 = "c" extends "a" | "b"? "x" : "y"; // "y"型 例えば、Conditional Typesを使うとif文が実現できます。 問題1 真偽値のリテラル型 C 、任意の型 T 、 F が与えられる。 C が true であれば型 T 、そうでなければ型 F を返すような型関数 IF<C, T, F> を作成せよ。 type-challanges 問題リンク type ex0 = If<true, "a", "b">; // "a"型 type ex1 = If<false, "a", "b">; // "b"型 回答は次のようになります。 解答1 type If<C extends boolean, T, F> = C extends true? T : F; true 型に代入可能な型は true 型のみなので、 C が true 型なら T 型を返し、そうでなければ F 型を返すということになります。 さて、Conditional Typesには条件文がありますが、なんとこの条件文で、新たな型変数を導入できます。 問題2 Promise<Type> 型が与えられるので、型 Type を得る型関数 Awaited を作成せよ。 type-challanges 問題リンク 解答2 type Awaited<T> = T extends Promise<infer R> ? R : never; infer R という構文が、 R という新たな型変数をこの後使いますよ、という意味です。 Promise の中身の型はわからなくても、TypeScriptが型推論をし、その型を返すような型関数を作成できるわけです。 Template Literal Types Template Literal自体はJavaScriptのES6から導入された機能で、文字列を自分で定義した変数を用いて作成できる機能です。これはお世話になっている人も多いと思います。 const fruit = apple; const applePie = `${fruit}Pie` // applePie = "applePie"となる これが型でもできるいうのがTemplate Literal Typesです。 type Fruit = "apple" | "peach"; type FruitPie = `${Fruit}Pie`; // FruitPie型は "applePie" | "peachPie" 型 こんなこともできます。 type ID = `Number : ${number}`; const ex0:ID = "Number : 1 " ; const ex1:ID = "Number : 12345 " ; const ex2:ID = "Number : abc " ; //compile error ID 型の定義のTemplate Literal Typesの部分に number が使われています。このように書くと、 ${number} の部分を数値型に置き換えることが可能な文字列を受け入れる型を定義できます。 このTemplate Literal Typesと、先ほど紹介したConditional Typesを用いると、型における文字列の操作が可能になります。 問題3 文字列のリテラル型 S 、 From 、 To が与えられる。 S を左からみたときに一番最初に部分文字列として現れる From を、 To に置換した型を返すような型関数 Replace を作成せよ。ただし、 From が空文字列の場合は置換しなくてよい。 type-challanges 問題リンク type ex0 = Replace<"types are fun!", "fun", "awesome"> //"types are awesome!"型 type ex1 = Replace<"aaa", "a", "b"> //"baa"型 一見大変そうですが、なんとこれが次のように書けてしまうことがTemplate Literal Typesの面白いところです。 解答3 type Replace<S extends string, From extends string, To extends string> = From extends '' ? S: S extends `${infer L}${From}${infer R}`? `${L}${To}${R}`:S; 3行目に注目してください。この条件文では、「 S という型が ${L}${From}${infer R} という形で書けるか」ということを判定しています。しかもこの一致判定は前方一致 6 なので、最も左に現れる文字列 From が ${From} に対応します。この条件が満たされれば、 ${L}${To}${R} という形で表される文字列のリテラル型を返すので、 From を To に置換することが達成されます。 問題4 文字列のリテラル型 S 、 From 、 To が与えられる。 S を左から順に、部分文字列として現れる From を、 To に すべて 置換した型を返すような型関数 ReplaceAll を作成せよ。ただし、 From が空文字列の場合は置換しなくてよい。 type ex0 = ReplaceAll<"types are fun!", "fun", "awesome"> //"types are awesome!"型 type ex1 = ReplaceAll<"aaa", "a", "b"> //"bbb"型 type ex2 = ReplaceAll<"aaa", "aa", "b"> //"ba"型 type ex3 = ReplaceAll<"ababa", "cc", "acccc"> // " aba ba"型 先ほどは左から見て最初の From を置換するだけでしたが、今回はすべて置換する必要があります。ここで大事なことは、Conditional Typesによって 再帰 が実現できるということです。 解答4 type ReplaceAll<S extends string, From extends string, To extends string> = From extends '' ? S: S extends `${infer L}${From}${infer R}`? `${L}${To}${ReplaceAll<R, From, To>}`:S; 先ほどの Replace 関数と違い、 ReplaceAll 関数ではConditional Typesの分岐後に R 型の部分に対して ReplaceAll を適用しています。このように再帰関数として型関数を定義することで、さまざまなことが型で実現できるようになります。 さらに文字列操作に関する問題を解く これらの機能を用いて、いろいろな問題を解いてみましょう。 問題5 文字列のリテラル型 S に対して、その長さを表す数値のリテラル型を返す型関数 LengthOfString を作成せよ。 type-challanges問題リンク type ex0 = LengthOfString<"abcda">; // 5型 type ex1 = LengthOfString<"a">; // 1型 普通の文字列であればその長さを取得できるメソッドがありますが、型には残念ながらありません。自分で書きましょう。 まず、以下のようにすれば、「文字列を先頭から一文字ずつ切り出して何らかの操作をする」ということができます。つまり、 for文が書けます 。 S extends `${infer L}${infer R}`? Lに対する何らかの操作+Rに対する再帰 : 停止操作 ${infer L}${infer R} の L の部分が前方一致となっており、 S の最初の1文字を切り出すということができます。 S の残りの文字列が R になります。 それならば、 Cnt という数値型を用意して、一文字ずつ切り出すごとに Cnt に1加える、ということを繰り返せばよさそうに思えますが、なんと「数値型に対して1加える」という操作が容易にできません。そこで、型で数値演算を扱う唯一の方法である タプル型 が登場します。タプルといっても実際は配列なので、ここでは詳細な説明は省略し 7 、その機能に注目しながら先に進みます。 実はこのタプル型には、その長さを取得する方法が用意されています。 type T = ["a", "b", "c"]; // タプル型 type length = T["length"]; // 3型 なんとタプル型 T に対して、 T["length"] でその長さを表す数値型を取得できます。そんなわけで、このタプル型をカウンターとして用いることにしましょう 8 。 元の問題に戻ります。「文字列 S を左から一文字ずつ切り出し、タプル型の要素に追加する」ことを繰り返せばよいです。全て追加し終えたら、タプルの型の長さを取得すればよいです。 解答5 type LengthOfString<S extends string, Cnt extends any[] = []> = S extends `${infer L}${infer R}`? LengthOfString<R, [1, ...Cnt]> : Cnt["length"]; 型関数も普通の関数と同様、デフォルト引数を設定できます( Cnt extends any[] = [] の部分)。 Cnt のデフォルト引数を空のタプルとして、カウンターを初期化します。 `${infer L}${infer R}` で一文字切り出し、 LengthOfString<R, [1, ...Cnt]> としてカウンターに何かしらの要素を追加します(1である必要性はなく、 "" でも十分です)。 ...Cnt という記法は配列やオブジェクトと同じで、タプル型の要素を展開するものです。 問題6 文字列のリテラル型 S に対して、 S が回文であれば true 型、そうでなければ false 型を返す型関数 IsPalindrome を作成せよ。 type-challanges問題リンク リンク先ではnumber型も考慮する必要がありますが、ここでは文字列型に限定しておきましょう。 type ex0 = IsPalindrome<"abcba">; //true型 type ex1 = IsPalindrome<"abcdef">; //false型 この問題はTypeScriptの型ではなく普段慣れ親しんでいるプログラミング言語で判定を書いてくださいと言われても、苦戦する人がいるかもしれません。文字列 S が回文であるということは、 S を反転させた文字列を revS とすると、 S と revS が一致することと同値です。したがって、 S を反転した文字列が得られればなんとかなりそうです。このためには、空文字列 T を用意し、 S を1文字ずつ切り出しながら、 T の先頭に追加していけばよさそうです。 type Reverse<S extends string, T extends string = ""> = T extends `${infer L}${infer R}`? Reverse<R, `${L}${T}`> : T; Conditional Typesの条件分岐後の Reverse<R, `${L}${U}`> という部分で、 S の先頭の文字を T の先頭に追加する、という操作が実現されています。ここまでできれば、リテラル型に関する型の一致判定は extends で十分なので、次のようにすればよいでしょう。 解答6 type Reverse<S extends string, T extends string = ""> = S extends `${infer L}${infer R}`? Reverse<R, `${L}${T}`> : T; type IsPalindrome<S extends string> = S extends Reverse<S>? true : false いかがでしょうか。型レベルでも結構いろんなことができそうな気持ちになってきたのではないでしょうか。これらを体得したいという型は、ぜひtype-challangesの問題に挑戦してみてください。また、本記事ではオブジェクト型に関する問題は扱いませんでしたが、TypeScriptを使いこなすうえでは大事なテーマかと思いますので、こちらも問題を見てみるとよいと思います。 最後に、もう一問問題を出題して本記事の締めくくりとしたと思います。本記事で紹介した知識のみで解くことができるので、ぜひ考えてみてください。 最後まで読んでいただきありがとうございました。 演習問題 文字列 S が 平方 であるとは、ある文字列 T が存在して S=TT と書けることをいう。つまり、 S がある文字列を2つ並べて得られる文字列であるとき、 S は平方であるという。 文字列のリテラル型 S が与えられる。 S が平方であれば true 型、そうでなければ false 型を返す型関数 IsSquare を作成せよ。 9
はじめに これは、FORCIA Advent Calendar 2021の16日目の記事です。 新卒1年目の井上と申します。本格的な業務を開始して以来、TypeScriptという言語を触ってきました。TypeScriptというのはその名の通り、JavaScriptに型を付けたような言語です。学生のころよく書いていた言語といえばC++やJavaなのですが[1]、どうやらそれらの言語よりも型でいろんなことができるようで、せっかくなのでTypeScriptの豊かな機能を学んでみたいとなんとなく思っていました。 そんなことを考えていたら、type-challangesというサイトの存在を教え
これは、 Qiita Advent Calendar 2021 GitLab の15日目の記事です。 はじめに こんにちは、 フォルシア にて、旅行会社向けの web アプリケーションを開発しています、エンジニアの高橋です。普段のアプリ開発の業務のほかに技術広報も兼任しており、弊社で開催しているアドベントカレンダーの運営もお手伝いしています。 フォルシアではもともと、社内のイベントとしてアドベントカレンダーを始めましたが、2018 年からは弊社ブログ( FORCIA CUBE )にて外部の方向けに記事を公開しています。 社内のみで記事を公開していた頃は、誰かが多少締め切りをすぎても「かまへんかまへん」と言えますが、外部に公開するとなるとそうはいきません。25 日間落とすことなく記事を上げ続けるために、執筆者へのフォローや締め切り管理、ブログへのアップロードを組織的に行う必要がありました。 外部公開を始めた初年度は esa というドキュメントツールのみで記事や進捗を管理していましたが、どの記事がいつ公開なのか、レビューが終わっているのかどうかなどを一覧で見ることができず、管理がなかなか大変でした。 そこで 2020 年より、社内ですでに使用していたコードのホスティングサービスである GitLab を利用して記事を管理するようにしました。GitLab の issue 機能を使い 1 issue = 1 記事に対応させて管理するようにしたことで、かんばんボードを使って記事の進捗を管理することができるようになり、大変便利でした。以下の図のように、カラムやラベルを付けて記事を管理しています。記事自体も issue に直接記載する形式にしました。 issue の利用により進捗の管理が容易になった点はよかったのですが、その年のアドベントカレンダー運営の振り返りでは以下のような課題も上がりました。 レビューがやや難しい せっかくなので、GitLab の CI 機能をもっと活用したい そこで今年のアドベントカレンダーでは、 GitLab の MR 機能(マージリクエスト。GitHub の Pull Request に相当する機能)を利用してみることにしました。 MR であれば 行単位でレビューが書けるため、指摘箇所がわかりやすい 編集履歴を残すことができる CI を活用しやすい といった利点があります。 さらに、せっかく CI を使えるようになったので以前からやってみたいと思っていた誤字脱字の指摘、文章の構成の自動化にとりくんでみました! その内容を本記事で紹介させてください。 できたもの 投稿された記事を校正し、MR にレビューコメントをしてくれる「赤ペン博士 bot」を作りました。 使用したライブラリの説明 textlint textlint は「linter」と言われるツールの一種で、記述されたプログラムや文章がルールに沿って書かれているかをチェックしてくれます。多くの linter がプログラミング言語を対象としてツールであるのに対して、textlint は自然言語を対象にしているため、文章の校正に利用できます。 textlint で適用するルールは様々なプラグインから自分で取捨選択でき、例えば以下のような項目をチェックできます。 文末が「。」で終わっているかどうか 日本語の誤用がされていないかどうか 必要な箇所にスペースが入れられているかどうか textlint の導入によりこれまで人力で指摘・修正していた文章の誤りを自動で検知することができるようになります。 Reviewdog Reviewdog は様々な linter と組み合わせて、GitHub や GitLab などのコードホスティングサービスに linter が指摘した内容をレビューコメントとして投稿してくれるツールです。これにより、GitLab に push された記事に対して、textlint を適用した結果を該当箇所にコメントできます。 余談ですが、ロゴが可愛いのも特徴です。 GitLab CI GitLab CI は GitLab が公式に提供している Continuous Integration(CI)ツールで、GitLab 上に push されたコードに対して、トリガーや実行内容を設定して処理を行うことができます。一般的なプロジェクトではテストやデプロイを自動化するために使われることが多いですが、今回は校正を自動化するために利用しました。 Reviewdog が MR にコメントするには GitLab でアクセストークンを発行する必要がありますが、Reviewdog 用の GitLab アカウントを新規で作成し、プライベートアクセストークンを発行することで実現しました。プロジェクト単位でアクセストークンを発行することもできますが、その場合アイコンを設定できないためレビューがややたんぱくになってしまうのが難点です。 上記のツールを組み合わせ、以下のような仕組みを作成しました。 MR が作成されたり更新されると自動で GitLab の CI が起動し、CI の中では文章に対して textlint で校正をかけ、その結果を Reviewdog が MR へのコメントとして通知してくれます。 各種設定について 次に、ツールを動かす設定をご紹介します。 package.json の設定 textlint は npm を使って導入できます。package.json を未作成のプロジェクトの場合、プロジェクトのルートディレクトリで npm init -y npm install --save-dev textlint とすることで、package.json の作成と textlint のインストールが行われます。textlint の拡張パッケージ(後述)についても同様に npm からインストールできます。 gitlab-ci.yml の設定 .gitlab-ci.yml ファイルをリポジトリに作成し、GitLab 側の設定をいくつかすることで CI 機能を使うことができます。もう少し詳しく知りたいという方は、よければ以下の記事をご覧ください。 GitLab CI/CD 導入の手引き(FORCIA CUBE) CI では使用するライブラリのインストールと、 textlint, Reviewdog の実行をしています。また、Reviewdog 実行用の API アクセストークンのキーは CI 設定の Variables に登録してあります。 image: alpine # CIで使用するdockerイメージの指定 reviewdog: allow_failure: true # CIに失敗してもMRをMerge可能なようにする設定 script: - apk add --update git - apk add --update npm - npm install # package.jsonに登録したtextlintのインストール # reviewdogのinstall # bin/ 以下に実行ファイルがインストールされます - wget -O - -q https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s # textlintとreviewdogの実行 # *.md ファイルを対象にlinterを実行しています - npx textlint -f checkstyle *.md | bin/reviewdog -f=checkstyle -name="textlint" -reporter=gitlab-mr-discussion textlint のプラグイン textlint には様々なプラグインがあり、自分の好みのルールを追加することができます。今回利用したのは以下のプラグインになります。 textlint-rule-preset-ja-spacing 日本語におけるスペースの使い方のルール textlint-rule-preset-ja-technical-writing 日本語による技術文書向けのルール 技術書向けなのでブログにはふさわしくないルールも多く、適宜取捨選択して利用しています textlint-rule-preset-jtf-style 日本語用の標準的な textlint のルール郡 textlint-rule-prh 文章の表記ゆれを検出するためのルール 自分で辞書を作成して、検出対象を決めることができる これらのプラグインの細かなルールは .textlintrc.js にて設定することができます。 今回は以下のような設定で利用しています。 module.exports = { plugins: { "@textlint/markdown": { extensions: [".md"], // マークダウン用の拡張 }, }, rules: { // textlint-rule-prhの設定 prh: { rulePaths: ["./prh.yml"], }, // textlint-rule-preset-jtf-styleの設定 "preset-jtf-style": { "1.2.1.句点(。)と読点(、)": false, // 文中のピリオドとカンマを許容 "1.1.3.箇条書き": false, // 箇条書きの文末に句点(。)以外を許可 "2.1.8.算用数字": false, // 算用数字以外も許容する。1桁は全角でも入力できるように。 "2.2.1.ひらがなと漢字の使い分け": true, // ひらがなにしたほうが良い漢字をサジェスト "4.1.3.ピリオド(.)、カンマ(,)": false, // 文中のピリオドとカンマを許容 "4.3.1.丸かっこ()": false, // 半角丸括弧を許容 "4.3.2.大かっこ[]": false, // 半角大括弧を許容 }, // textlint-rule-preset-ja-technical-writingの設定 "preset-ja-technical-writing": { "no-exclamation-question-mark": { allowFullWidthExclamation: true, allowFullWidthQuestion: true, }, "no-doubled-joshi": { strict: false, allow: ["か", "が", "に"], // これらの助詞は同一文中に多く登場しても許容 }, }, // textlint-rule-preset-ja-spacingの設定 "preset-ja-spacing": { "ja-space-around-code": { before: true, after: true, }, }, "ja-technical-writing/ja-no-mixed-period": { allowPeriodMarks: [":"], }, "ja-technical-writing/max-ten": { max: 5 }, // 文中の「、」の数は5個まで "ja-technical-writing/sentence-length": false, // 文の長さは指定なし "ja-technical-writing/ja-no-weak-phrase": false, // 弱い表現を許容 "ja-technical-writing/max-comma": false, // カンマの数は指定なし "ja-spacing/ja-space-around-code": false, // インラインコードの前後にスペースを入れなくてもよい }, }; phr.yml の設定 前述した textlint のプラグインの一種である textlint-rule-prh を用いることで、表現の揺れを統一することができます。 phr.yml に以下のようなルールを登録して、ライブラリ名の揺れなどを統一しました。 version: 1 rules: - expected: Docker pattern: docker specs: - from: docker to: Docker - expected: Ansible pattern: ansible - expected: Ansistrano pattern: ansistrano - expected: Kubernetes pattern: kubernetes - expected: React pattern: react - expected: Redux pattern: redux - expected: Next.js patterns: - next.js - Nextjs 以下のように揺れを指摘してくれます。 おわりに 今回の仕組みを導入したことにより、人力で指摘するのはハードルが高い内容も自動でレビューできるようになり、運営チームの負担を減らしつつ記事の質を高めることができるようになったと思います。 また個人的に嬉しかったこととしては、記事を書いてくださる方達も赤ペン博士によるレビューを面白がってくださったことです。煙たがられるかなとも思っていたので、楽しんでいただけたようで作った甲斐がありました。 それでは残りの記事もお楽しみに!
これは、[Qiita Advent Calendar 2021 GitLab](https://qiita.com/advent-calendar/2021/gitlab)の15日目の記事です。 はじめに こんにちは、フォルシアにて、旅行会社向けの web アプリケーションを開発しています、エンジニアの高橋です。普段のアプリ開発の業務のほかに技術広報も兼任しており、弊社で開催しているアドベントカレンダーの運営もお手伝いしています。 フォルシアではもともと、社内のイベントとしてアドベントカレンダーを始めましたが、2018 年からは弊社ブログ(FORCIA CUBE)にて外部の方向けに記
これは、 FORCIA Advent Calendar 2021 の14日目の記事です。 こんにちは。新卒2年目エンジニアの三浦です。 突然ですがみなさんは会社の同期のやっている仕事、知っていますか? フォルシアでは、全く決まりや強制ではないのですがいつからか新卒エンジニアは代々同期同士の気軽な情報共有の場を設置して、コミュニケーションをとる習慣があります。 代によって頻度や形式など違いはありますが、私を含む新卒2年目(20期入社)の同期でも「20期エンジニアのつどい」と題して行っており(以下、これを単に「つどい」と呼びます)、個人的にはとてもよい取り組みだと感じています。 今回は、そんな「つどい」について私のおすすめポイントを含めてご紹介できればと思います。 「つどい」って具体的にどんなことやるの? 形式としては 頻度は週1回45分 内容は技術寄りの情報共有 その回で話題を一つ提供するメインスピーカー もちろん、話したいネタがある人はいつでも好きなタイミングで話題提供してOK メインスピーカーはローテーションの持ち回り となってます。 情報共有というと少し硬い印象を受けるかもしれないですが、同期だけなのでとてもカジュアルな雰囲気でやっています。フォルシアではエンジニア全体の情報共有の場も別にありますが、そこで話すほどでない些細なレベルの話でも共有しています。 具体的な話題としては以下のような感じです。 仕事の内外で学んだTips共有 最近している仕事の内容共有 ある程度仕事に関係したお悩み相談 (たまに)雑談 最近ですと、「 CORS で詰まって、こう解決したよ」や「こんなふうにデータを取り出したくて、こんなSQLを書きました」といったことや、このアドベントカレンダーでも近日公開されるような「TypeScriptでこんな型パズルしてみました」という話などを共有しました。 また直接仕事と関連していない、興味で触ってみた技術の話なども共有しています。 やっていることはシンプルですが、社外で同じようなことをしているという話はあまり聞いたことがありません。おすすめできる点が3つあるので、順にご紹介します。 1. 幅広い学びがある 自分とは違うプロジェクト・チームにいる同期から、その中であった学びが提供されるのでかなり新鮮なことも多いです。自分の業務の中だけだと限られた範囲の技術ばかり触りがちになりますが、「つどい」があることで知る機会がなかった技術の話が聞けます。 技術だけではなく、業務の進め方やチームの運用方法などの面でも学びがあり、いい面を自分の業務に取り入れることもできます。 例えば、同期がTipsとして紹介していた「簡単なテキスト操作のためのワンライナーコマンド」であったり、「案件の見積もり作業に当たって注意したこと」などは今の自分の業務の中でも生きている学びでした。 2. メインスピーカーの持ち回りでメリハリが生まれる メインスピーカーを持ち回るのは結構重要なルールだと思っています。 ただのゆるゆる雑談ではなく、その週担当の、メインで話題を提供する人がいることで、提供者にとっては学びになることを探そうというモチベーションに繋がります。多少はプレッシャーにもなりますが、持ち回ることで二ヶ月に一回くらいの頻度となり、大した負担ではありません。 話題がある人はesaに簡単な資料を用意するので、共有する情報について提供者自身の中で整理でき、また共有することで人から質問がもらえるので、学びも深まります。 3. コミュニケーションが増える フォルシア内での新卒エンジニアの同期間の情報共有はコロナ以前から行われていましたが、特に今のリモートワーク前提のご時世では、コミュニケーションの機会として真価が発揮されているような気がします。対面でのちょっとした雑談や飲み会などの機会が激減している中、週に一回同期と話す習慣があるのはとても楽しいです。このおかげであまり会えなくても同期の仲が深められていると思っています。 また冒頭の質問のように、一緒に入った同期が今どんな仕事をしているか、なども自然に話せてお互いの頑張りが知れるのも良い点です。 時にはエンジニアだけでなく総合職の同期も参加し、総合職目線の話が共有されたりして新鮮でもあります。週一のとても気軽な同期会、ということでちょうど良いコミュニケーションだと思っています。 まとめ 直接は集まれない、あまり出社しないので雑談も生まれない、そんな中でも仕事の話と雑談の間のちょうど良い塩梅で学びがある、週に一回短時間の同期のコミュニケーションです。 昨年度の末には、1年目の仕事の振り返りをしてそれぞれ共有する会もありました。そんな感じで、気軽に内容をアレンジしても良さそうです。 社外の方が「つどい」ライクなものを実践される際の汎用的なポイントとしては 習慣化して定期的に行う会とする 内容は準備が負担になりすぎないレベルのものにする 気軽に質問してカジュアルな雰囲気を作っていく といったことを意識していただくのが良いかと思います。 みなさん、ぜひ取り入れてみてはいかがでしょうか。
こんにちは。新卒2年目エンジニアの三浦です。 突然ですがみなさんは会社の同期のやっている仕事、知っていますか? フォルシアでは、全く決まりや強制ではないのですがいつからか新卒エンジニアは代々同期同士の気軽な情報共有の場を設置して、コミュニケーションをとる習慣があります。 代によって頻度や形式など違いはありますが、私を含む新卒2年目(20期入社)の同期でも「20期エンジニアのつどい」と題して行っており(以下、これを単に「つどい」と呼びます)、個人的にはとてもよい取り組みだと感じています。 今回は、そんな「つどい」について私のおすすめポイントを含めてご紹介できればと思います。 「つどい」
これは、 FORCIA Advent Calendar 2021 の13日目の記事です。 こんにちは。DXプラットフォーム部のエンジニアの伊藤(亜紀)です。 データクレンジングツールMasstery の開発を担当しています。 この記事のテーマは「ソースコードレビュー」です。突然ですが皆さん、ソースコードレビューはどのように実施されていますか? 頭では重要とわかっているつもりだけれども、手を回せていない やってはいるけれど、時間をかけすぎている気がする やってはいるけれど、何をどこまでやれば十分か、悩ましい そんな方も多いのではないでしょうか。 私が担当しているMassteryの開発チームでは、ちょうど1年ほど前にソースコードレビューのやり方を変更しましたが、ソースコードレビューが開発速度の向上に寄与している実感を持てています。 今日はそのソースコードレビュー、題して「開発速度志向のソースコードレビュー」について、ご紹介させていただきます。 最適なソースコードレビューの進め方は、アプリ・チームの規模・求められるサービスレベル・リリース頻度等々によって異なりますが、何かしらヒントになる情報をご提供できたら幸いです。 Masstery開発チームでソースコードレビューを変更した経緯 Masstery開発チームでは、ちょうど1年ほど前、開発メンバーが2人から3~4人にふえたタイミングから、ソースコードレビューのやり方を変更しました。 それまでは2人目メンバー(アプリ習熟度が浅い)が開発したら、マージリクエスト(プルリクエスト)を作成して、1人目メンバー(アプリ習熟度が深い)に見てもらう、というやり方でしたが、新しいメンバーがふえたことで 必要なレビューの総量がふえた 大きめの機能追加が並行するようになり メンバー同士、最新のアプリ状態にキャッチアップが追いつかないことがふえた コンフリクト(ソースコード・仕様とも)がふえた という状況になったためです(※)。 とくに後者について、 キャッチアップ不足に基づきリリースに不具合が混入し、緊急対応が必要になる コンフリクトの解消に時間を取られる 他メンバーが実装した箇所に改修を加えたくなったときに、ソースコードの理解に時間がかかる といったことで、開発の進行に影響が出ていた点が懸念でした。 そこで、このタイミングで開発速度を向上させるためのソースコードレビューを考えました。 過去に所属していたチームのソースコードレビューのいいとこどりをしながら、Masstery開発チームにとって最適なやり方を検討しました。 ※参考:リリースは週1回で、開発メンバーごとに開発範囲を決めることはしていません。 基本的な考え方 具体的なやり方ついてお話する前に、まずは基本的な考え方についてピックアップしてご説明します。 開発メンバー全員参加のMTGでレビューする レビューは基本的に、開発メンバー全員参加のMTGで行います。Masstery開発チームでは毎週火曜と木曜の週2回、ソースコードレビューのMTGを設けています。週2回では足りない場合や多すぎる場合には、適宜調整しています。 正直なところ、定期開催・全員参加のMTGにするかどうかは悩んだポイントでした。個々人の開発時間を確保するために、できるだけMTGは減らしたいと考えていたためです。 ただ最終的に、全員がアプリの状況を把握するからこそのメリットの方が大きいと判断し、定期開催・全員参加にしました。 全員参加のメリットについては後ほどご説明します。 「全員がいち早く"概要理解"に達する」ことを目標に、実装者が口頭で説明をする 開発速度向上を目的としているので、レビューでまずめざすゴールは、いろいろ欲ばりたい気持ちを抑えて「全員が対応の概要を理解すること」としました。 ここで「対応の概要」というのは「なぜ・何を・どのように実装したか」です。 なぜ:対応の目的 何を:どの機能を追加or修正したか。ユーザや外部連携システムから見た変更点は何になるか どのように:ソースコードの変更要点。その設計思想 受け売りを含んでもよいので、とにかく全員が上記のポイントを抑えた状態をめざします。 そして、その状態にいち早く達することができるよう、レビューは 実装者が、ソースコードの差分をレビュアーに見せながら、対応概要を口頭で説明 レビュアーは、説明を聞いている途中で疑問があれば、その場で質問 という進め方をします。実装者自身が着目すべき差分をピックアップすること、そしてインタラクティブに進めることで、素早い理解につなげます。 「全員が対応の概要を理解」した後は、レビュアーから「このケースの動作確認はきちんとできている?」「こう書かないとメンテナンス性を損なうのでは?」といった、マージするために必要な質問・コメントを行います。 ただしあくまで「対応概要の説明を聞いたうえで気になった範囲」で行います。 このゴール設定は重要なポイントなのですが、その理由については後ほどご説明します。 極力その場でマージする。開発を止めない 「開発の効率をあげる」ことが一番の目的です。そのため、できる限りレビューの場でマージまでするように心がけます。 その場でマージすることが難しい場合も、「この点がクリアされれば、セルフマージしてOK」というように、マージ条件をレビュー中に明示するようにします。 もちろん、バグや、メンテナンス性を著しく損なうような変更をリリースしては問題なので、そのような場合は修正後のマージとします。 しかし開発速度が最優先であり、「たしかにそう書いた方がきれいだけど、今後の開発効率に大きく効いてこないのでは?」という場合はマージします。 特に不具合修正やユーザビリティ改善の場合、ユーザに早く快適に使ってもらえるようにすることが先決なので、その場はマージしたうえでリファクタリングは別のタスクに切り出すことも多いです。 具体的な運用 運用は以下のとおりです。 準備 実装者は、マージリクエスト(プルリクエスト)を作成する。 「機能の概要」「ソースコードの変更点」「動作確認内容」「特に見てもらいたい点」を簡単に記載します しっかり書くとそれだけで時間がかかってしまうので、「簡単に記載」がポイントです! スムーズに説明するためのカンペ程度に書いています。 実装者はレビューMTG開始前までに、今日のMTGで見てほしいマージリクエスト(プルリクエスト)と、その説明所要時間を申告 特別な仕組みはなく、slackに書いています。 大きな対応でも、説明所要時間は最大10分程度におさまるようにします。 どうしても10分でおさまらない大作は、レビュー効率も悪くなりがちなので、マージリクエスト(プルリクエスト)を作成する段階で、レビュー単位が大きくなりすぎないよう気をつけます(大枠ができた段階で一度見てもらうなど)。 レビューMTG 30分のMTGで、チーム全員でレビューします。 申告されたマージリクエストのうち、対応期限などを考慮してその日見るものを相談・決定 実装者は、ソースコードの差分を見せながら、対応概要を説明 「なぜ・何を・どのように実装したか」「動作確認の内容」「特に見てもらいたい点」 レビュアーは、疑問があれば適宜質問 説明後、レビュアーから気になった点があればコメントする 問題なければその場でマージ 問題がある場合も、どうなったらマージ可能か、できるだけその場で明確にする メリット この進め方は開発速度を重視したものですが、開発速度以外にも以下のようなメリットがあります。 悩む時間がほとんどない 実装者が対応について口頭で説明し、インタラクティブに質問しながら進めるので、レビュアーが悩む時間が少なくて済みます。 マージリクエスト(プルリクエスト)に対応内容を丁寧に記載する方法は、記録がしっかり残る点はよいのですが、文章で説明している対応内容とソースコード変更点の対応づけに、地味に時間がかかるものです。 とくに、アプリ習熟度が浅いメンバーほど、ちょっとしたことで理解に躓きがちで、しかも1つ躓くとその先が理解できないことも多いです。 それを都度マージリクエスト(プルリクエスト)コメントで訊いていると、待ち時間を含めて理解までにかなり時間がかかってしまいましたが、インタラクティブに進めることでその問題は解消します。 さらに、MTGは時間の制約があるので、つい深入りして時間をたくさん使ってしまった、ということも防ぎやすいです。 アプリ固有知識の平準化が図れる 全員でレビューすることで、アプリの固有知識や設計思想を共有することができます。 そういった知識は、ドキュメントや概念図にまとめられているのが理想ではありますが、実際の開発現場では 変わらないと思っていた要件・前提・設計がどんどん変わり、ドキュメントの更新が追いつかない どうせすぐ書き換えることになるので、ドキュメントには大枠だけ書いておき、あとはソースコードを正とする 大きな変更が並行して進行する などといったことは日常茶飯事で、ドキュメントに落とし込みきれていない暗黙知というのはどうしても生じてしまうものです。 ですが、対応概要だけでもレビューしていれば「こういう設計になっているのでこう対応した」という理解を積み上げていくなかで、暗黙知をレビューの場で補完していくことができます。 さらに全員いると、アプリ固有知識への習熟度が高い人と低い人が必ず同席するので、「詳しくない人が調べる」のではなく「詳しい人が説明する」という方法で疑問を解決できるので、アプリ固有知識の平準化はさらに促進されます。 また、 全員が理解していれば、リリース後しばらくして不具合が発覚したときに、仮に実装者やその箇所に詳しい人が不在だったとしても、原因のアテがつけやすい 全員が最新の開発を把握していると、コンフリクト(ソース・仕様とも)に気が付きやすい というのもメリットです。 レビューが滞留しない 1~2人にレビューを割り当てる方式だと、レビュアーが忙しかったり割り当てられたことを見落としたりすると、そこでレビューが滞ってしまいます。 そこは「優先度をあげて、きちんとやりましょう」という話ではあるのですが、他ならぬ私も、レビュータスクが苦手な人間です(笑)。 たくさんタスクを持っているときに、差し込みのタスクが1つ飛んでくると、気づかぬうちにちょっとした心理的負担になってしまうのですよね。 多くのレビューは数分では完了しないので、完了するまでの間「やらねばやらねば」とレビュータスクがTODOリストと頭の中に存在し続けますし、緊急のものでなくとも相手を待たせると小さな罪悪感を感じてしまいます。 レビューを定期MTGにして、そこで全員の知見を結集して素早く疑問や懸念を解決し、マージしていくようにすれば、レビューが溜まりません。 もちろんMTGなので、毎度待ち時間のオフセットはつきますし固定の拘束時間にはなるのですが、それでもトータルで見ると快適になったと感じています。 レビュータスクが特定の人に集中しない レビューを全員でやると決めてしまうことで、特定の人(そのアプリに最も習熟している人)ばかりレビューすることを防げます。 時折「ここの実装は問題ないか、後でxxさんに詳しく見てもらった方がいいね」となることはありますが、本当にその人が入念に見るのが最適なものだけに限定することができます。 知識の平準化を図ることで、「対応内容の妥当性をチェックできる人」がふえていくので、そういった「入念な確認」も徐々に分散させていくことができます。 実はこれだけで品質担保にもかなり役立つ 開発速度向上を志向したソースコードレビューについてお話してきました。 レビューの第1のゴールはあくまで「全員が対応の概要を理解する」ことで、対応に問題がないかのチェックはあくまで「説明を聞いて気づいた範囲」で行うことを意外に感じられた方もいらっしゃるかもしれません。 このやり方ではバグを発見しきれず品質は担保が不十分になるのではとも感じられたかもしれません。 私は、これでも品質の担保にもかなり貢献していると感じています。それは、以下の理由からです。 (なお大前提として、バグはレビューだけで100%検知しようとするものではなく、テストやソースコードの質などで総力的に品質担保すべきものと考えています。) 口頭で説明すると問題点に気付きやすい これは個人的な経験則ですが、文字で説明するよりも口頭で話した方が問題点に気づきやすいと思います。 実装者は自身の対応内容を口頭でレビュアーに説明しますが、話している最中に実装者自身が問題点に気づくことがよくあります。 また口頭で説明すると、理解度・自信度合い・迷いなどが口調に如実に表れるので、問題点を検出するうえで重要な手がかりになります。 他メンバーの「視点」が入るだけでも問題発見効果がある これも経験則になってしまいますが、本当に致命的な不具合というのは 要件や仕様の理解不足 そんなことを考慮する必要があるとは思ってもみなかった そんなところに影響するとは思ってもみなかった というケースが多いです。ローカルなところで起こるというよりもグローバルなところで起きます。 こういったケースは、ソースコードやテストケースとにらめっこせずとも、「わかっている人」が見ればすぐに気付けます。 その点、異なる視点から「この観点には気をつけた?」というキャッチボールをたくさんできる進め方は効率的です。 もちろん、話した結果としてソースコードやテストケースを熟読する必要があると判断した場合は、熟読します。 さいごに いかがでしたでしょうか。 冒頭でもお伝えしたとおり、最適なソースコードレビューの進め方はアプリ・チームの規模・求められるサービスレベル・リリース頻度等々によって変わってきます。 とくに開発メンバーの人数がふえたらどうするかは悩ましい点ですが、今後も状況変化に合わせて試行錯誤と改良を加えていきたいと思います!
こんにちは。DXプラットフォーム部のエンジニアの伊藤(亜紀)です。 データクレンジングツールMassteryの開発を担当しています。 この記事のテーマは「ソースコードレビュー」です。突然ですが皆さん、ソースコードレビューはどのように実施されていますか? 頭では重要とわかっているつもりだけれども、手を回せていない やってはいるけれど、時間をかけすぎている気がする やってはいるけれど、何をどこまでやれば十分か、悩ましい そんな方も多いのではないでしょうか。 私が担当しているMassteryの開発チームでは、ちょうど1年ほど前にソースコードレビューのやり方を変更しましたが、ソースコードレビ
これは、 FORCIA Advent Calendar 2021 の12日目の記事です。 はじめに 「CDN」という仕組みをご存知でしょうか。 インターネットを支える縁の下の力持ち的な存在ですが、実際に意識することは少ないかもしれません。 ここでは「CDN」がどのように動いているのか、digコマンドとcurlコマンドを使って理解してみようと思います。 CDNとは 「CDN」とは「content delivery network」の略称でCDN事業者のサーバを経由してコンテンツを配信する仕組みです。 ごく簡単に図で表すと以下のようなイメージになります。 CDNを使用しないで配信する場合: [ユーザ]----HTTP(S)---->[配信元サーバ] CDNを使用して配信する場合: [ユーザ]----HTTP(S)---->[CDN事業者のサーバ]----HTTP(S)---->[配信元サーバ] つまり、インターネットにアクセスした際に私たちがアクセスしているサーバは、Webサイトの運営者のサーバではなくCDN事業者のサーバであることもある、ということになります。少し意外かもしれません。 なぜ、このような構成がとられるのでしょうか。以下のようなメリットがあると言われています。 CDN事業者のサーバにコンテンツをキャッシュすることによって、高速な配信が期待できる 急激にトラフィックが増えた場合に、CDN事業者のサーバでトラフィックを吸収できるので、サービスがダウンしにくい ユーザの接続はCDN事業者のサーバに対して行われるため、配信元サーバが直接的な攻撃にさらされにくい ここで、以下のような疑問を持つ方もいるかもしれません。 「今どきのインターネット通信はほとんどがHTTPS通信で行われている。HTTPS通信はエンドツーエンドで暗号化されているため、CDN事業者のサーバでコンテンツをキャッシュすることなどできないのでは?」 「普通に」プロキシサーバーを立てるだけではその通りなのですが、一般的にCDNではHTTPS通信においてもキャッシュが利用可能です。 なぜなら、CDN事業者のサーバには配信元サーバと同じコモンネームのSSL/TLS証明書をインストールしてあるため、暗号化された通信の復号が可能だからです。 (逆に言えば、自身のサイトをCDN化する際にはCDN用に証明書を発行する必要があるということになります) [ユーザ]<=>[CDN事業者のサーバ]の通信はCDN事業者のサーバにインストールされた証明書によって暗号化され [CDN事業者のサーバ]<=>[配信元サーバ]の通信は配信元サーバにインストールされた証明書によって暗号化され それぞれ通信が行われている、ということになります。 アクセス先を確かめてみる では、Webサイトの運営者のサーバではなく、CDN事業者のサーバに接続されていることを確かめてみましょう。 アメリカのニュースサイト The New York Times は「Fastly」というCDN事業者によって配信されていますので、このサイトを例に見てみましょう。 弊社社屋からdigコマンドを実行すると以下のような結果が得られます。 $ dig www.nytimes.com <抜粋> www.nytimes.com. 381 IN CNAME www.prd.map.nytimes.com. www.prd.map.nytimes.com. 1 IN CNAME nytimes.map.fastly.net. nytimes.map.fastly.net. 30 IN A 151.101.229.164 「CNAME」というレコードが返却されていますが、これはCanonicalName(正規ホスト名)を指すものです。 つまりこの応答を読み解くと www.nytimes.com の正規ホスト名は www.prd.map.nytimes.com なので引き直してください www.prd.map.nytimes.com 正規ホスト名は nytimes.map.fastly.net なので引き直してください nytimes.map.fastly.net のIPアドレスは 151.101.229.164 です。 ということになります。 www.nytimes.com の最終的なCNAME先が nytimes.map.fastly.net つまりFastly社(CDN事業者)のドメイン名になっており、Fastly社が管理する権威DNSサーバによってIPアドレスが解決されていることが分かります。 また解決されたIPアドレス 151.101.229.164 をwhoisコマンドで引いてみると 以下のようにFastly社所有のものであることが分かります。 $ whois 151.101.229.164 <抜粋> NetRange: 151.101.0.0 - 151.101.255.255 Organization: Fastly (SKYCA-3) 以上のことから、ブラウザに https://www.nytimes.com/ と入力した際の通信相手は、「The New York Times」社のサーバではなく、「Fastly」社のサーバであることが分かります。 どのように配信サーバを割り当てているか CDN事業者はインターネット上に配信サーバを多数設置していると言われています。 Fastly社でも配信サーバを 世界各地 に配置しています。(日本国内にもあります) では、どのようにして多数あるサーバの中からアクセスさせるサーバを割り当てているのでしょうか? 先ほど解決されたIPアドレス 151.101.229.164 にpingを打ってみると数msで返ってくることから、サーバは日本国内にあることが推測されます。 $ ping 151.101.229.164 PING 151.101.229.164 (151.101.229.164) 56(84) bytes of data. 64 bytes from 151.101.229.164: icmp_seq=1 ttl=55 time=3.38 ms 64 bytes from 151.101.229.164: icmp_seq=2 ttl=55 time=3.20 ms <以下略> ではDNSサーバを変えて名前解決をしてみます。 ここでは日本から遠そうなロシアの検索サービスである yandex社のDNSサーバ である 77.88.8.8 を使ってみます。 $ dig www.nytimes.com @77.88.8.8 <抜粋> www.nytimes.com. 500 IN CNAME www.prd.map.nytimes.com. www.prd.map.nytimes.com. 120 IN CNAME nytimes.map.fastly.net. nytimes.map.fastly.net. 30 IN A 151.101.245.164 151.101.245.164 に解決されました。(先ほどは 151.101.229.164 だったので、第3オクテットのみが異なっています) pingを打ってみると200ms以上かかっており、日本からはネットワーク的に遠く(おそらくロシア付近)にあることが推測されます。 $ ping 151.101.245.164 PING 151.101.245.164 (151.101.245.164) 56(84) bytes of data. 64 bytes from 151.101.245.164: icmp_seq=1 ttl=51 time=269 ms 64 bytes from 151.101.245.164: icmp_seq=2 ttl=51 time=269 ms <以下略> つまり、同じホスト名を問い合わせているのにもかかわらず、問い合わせ元のDNSサーバによって解決するIPアドレスを変えていることが分かります。 [日本国内のクライアント]--DNSクエリ-->[日本のDNSサーバ]--DNSクエリ-->[CDN事業者の権威DNS] =>日本からのアクセスとみなして日本国内のサーバのIPアドレスを返却 [日本国内のクライアント]--DNSクエリ-->[ロシアのDNSサーバ]--DNSクエリ-->[CDN事業者の権威DNS] =>ロシアからのアクセスとみなしてロシア付近のサーバのIPアドレスを返却 このようにして、クライアントに対してネットワーク的に近い位置のサーバを割り当てていることが推測できます。 ECS(EDNS Client Subnet)について ここまで読み進めてくださった賢明な読者の方なら、以下の疑問を持つかもしれません。 「あの有名なGoogle Public DNSである8.8.8.8に問い合わせたらどうなるの?」 問い合わせ元のDNSサーバのIPアドレスのみを元にして返却するIPアドレスを制御する、という仕組みには限界があり 実際に、過去にはこのようなPublicDNSを使用した場合、 不適切なサーバが割り当てられる問題 があったと言われています。 ただし現在は ECS(EDNS Client Subnet) という仕組みで解決されています。 これは、DNSキャッシュサーバがDNS権威サーバに問い合わせる際に、DNSキャッシュサーバに問い合わせをしたクライアントのIPアドレス情報も伝達してしまう、という仕組みになります。 簡単に図で表すと以下のようになります。 [クライアント]--DNSクエリ(1)-->[PublicDNSサーバ]--DNSクエリ(2)-->[CDN事業者の権威DNS] ※この「DNSクエリ(2)」内に「DNSクエリ(1)」の発信元クライアントのIPアドレス情報が含まれている CDN事業者の権威DNSはクライアントのIPアドレス情報を元に最適なサーバのIPアドレスを返却するため、ECSに対応しているDNSサーバであれば現在このような問題は発生しないと言えます。 (逆に言えば前述のyandex社のDNSサーバはECSに対応していないものと考えられます) curlでアクセスしてみる では実際にWebサイトにアクセスしてみましょう。 CDN事業者によってデバッグ用コマンドのようなものが用意してあり、キャッシュにヒットしたかどうかなどが分かるようになっています。 Fastlyの場合 を例にして、curlコマンドで Fastly-Debug:1 をリクエストヘッダに付与して「The New York Times」のトップページに何度かアクセスしてみます。 $ curl -s --head -H "Fastly-Debug:1" https://www.nytimes.com/ -w "time_total:%{time_total}\n" | grep -e 'x-cache:' -e 'time_total:' x-cache: MISS, MISS time_total:1.036584 $ curl -s --head -H "Fastly-Debug:1" https://www.nytimes.com/ -w "time_total:%{time_total}\n" | grep -e 'x-cache:' -e 'time_total:' x-cache: MISS, HIT time_total:0.032943 x-cache レスポンスヘッダでキャッシュにヒットしたかを確認できるので比較すると 最初のアクセスではキャッシュがない( MISS )ためレスポンスに1秒以上かかってるのに対し、 その後のアクセスではキャッシュにヒット( HIT )し0.03秒程度でレスポンスされていることが分かります。 「The New York Times」の配信元サーバはおそらくアメリカ国内にあるものと思われますが、キャッシュにヒットしない場合はアメリカ国内のサーバと通信するためレスポンスに時間がかかっているのに対し、キャッシュにヒットした場合は日本国内のサーバからレスポンスされるため非常に高速にコンテンツが取得できています。 まとめ 以上のことから分かったことをまとめてみます。 私たちが普段アクセスしているサーバは、Webサイト運営者のサーバではなくCDN事業者のサーバであることもある CDN事業者のサーバかどうか、どのCDN事業者を使用しているかはdigコマンドで分かる CDN事業者のサーバは世界各地にあり、最適なサーバが割り当てられるようになっている サーバの割り当てはCDN事業者の権威DNSにより問い合わせ元のIPアドレスを元に行われている CDN事業者のサーバのキャッシュからコンテンツが返却される場合は非常に高速である CDNは目に見えないものなので普段意識することもあまりないと思いますが、このような仕組みでインターネットが動いているというを知っておくと何かの役に立つかもしれません。(立たないかもしれません)
はじめに 「CDN」という仕組みをご存知でしょうか。 インターネットを支える縁の下の力持ち的な存在ですが、実際に意識することは少ないかもしれません。 ここでは「CDN」がどのように動いているのか、digコマンドとcurlコマンドを使って理解してみようと思います。 CDNとは 「CDN」とは「content delivery network」の略称でCDN事業者のサーバを経由してコンテンツを配信する仕組みです。 ごく簡単に図で表すと以下のようなイメージになります。 CDNを使用しないで配信する場合: [ユーザ]----HTTP(S)---->[配信元サーバ] CDNを使用し
これは、 FORCIA Advent Calendar 2021 の11日目の記事です。 こんにちは! 旅行プラットフォーム部エンジニアの恒川です。 今年10月に入社し、毎日JavaScriptを書いています。 この記事では、JavaScriptのsymbolから始めて、「名前衝突」をキーワードに、それを利用したLispプログラムまで紹介したいと思います。 JavaScriptのsymbol symbol はES2015で追加されたプリミティブです。プリミティブとはメソッドを持たないデータのことで、 42 、 "Brendan Eich" などの仲間です。 symbol型のデータは関数 Symbol() の戻り値として生成できます。 console . log ( typeof Symbol ()) // symbol symbolに対してどんな計算ができるのでしょうか。 console . log ( Symbol ()) // Symbol() console . log ( Symbol (). toString ()) // 'Symbol()' ' Tell me your name, ' + Symbol () // Uncaught TypeError: Cannot convert a Symbol value to a string ほとんど何もできません。 symbolに対してstringと + の演算はできませんし、 .toString() を使っても 'Symbol()' が返ってきます。 恥ずかしがり屋さんなsymbolですが、 Symbol() で返されるデータはユニークであるという特徴をもっています。 console . log ( Symbol () === Symbol ()) // false つまり、 Symbol() は呼び出される度にこの世界でまだ一度も作られたことのないことを保証するsymbolを作るということです。 名前衝突を回避するJavaScriptプログラム(標準オブジェクトの拡張) symbolの名前が被らないという性質を使って、「名前衝突」を防ぐことができます。 例として、以下のようなStringオブジェクトに star() メソッドを追加してみます。 String . prototype . star = function () { return " * " . repeat ( 10 ) + this + " * " . repeat ( 10 )} console . log ( " Tsunekawa " . star ()) //**********Tsunekawa********** star() メソッドを使うと画面が華やかになって良いと思います。しかし、このように標準のオブジェクトにメソッドを追加するコードは非常に危険であると知られています。なぜなら、もし将来JavaScriptに star という名前の違う動作をするメソッドが追加された場合に既存のコードが動かなくなってしまうからです。また、 star を別の仕様で実装しているライブラリを使用した場合も同様です。 名前が競合することによってプログラムが意図しない動作をする状態を名前衝突と呼びます。非常にまずい問題ですが、以下のようにsymbolを使用することで名前衝突を避けることが可能です。 const star = Symbol (); String . prototype [ star ] = function () { return " * " . repeat ( 10 ) + this + " * " . repeat ( 10 )}; exports . star = star ; var s = require ( ' ./star.js ' ); console . log ( " Tsunekawa " [ s . star ]()); //**********Tsunekawa********** star.js ではsymbolを使ってStringオブジェクトに star() メソッドを追加し、そのsymbolをexportしています。 main.js ではexportされたsymbolを使って star() メソッドにアクセスしています。 star.js で作成されたsymbolは世界の誰とも被らない名前が付いているため、今後どんな拡張があったとしてもこのコードは動作します。この「名前衝突を回避することで互換性を維持したまま拡張をする」ことこそがsymbolの真骨頂と言えると思います。 Lispのsymbol JavaScriptのsymbolは名前衝突を回避するという、ある意味特別な目的のために導入されたデータ型でした。しかし、古い言語の中には基本的なデータの1つとしてsymbolを使っている言語がいくつか存在します。ここからはそんなsymbolと仲良しの言語、Lispを紹介していきます。 Lispは1950年代後半に登場した古いプログラミング言語です。1950年代がどれぐらい昔かというと、トランジスタが発明されたのがちょうどこの頃ですので、コンピューターも真空管からトランジスタへという時代でした。 そんな大先輩Lispの最も基本的なデータ型はsymbolでした。 coffee 、 *McCarthy* 、 + はLispのsymbolです。 Lispではしょっちゅう以下のようにsymbolを並べたリストを使って計算をします。 ( car ( cdr ( cons 'hoge ( cons 'fuga ( cons 'bar ( )))))) ; FUGA Lispの方言の1つであるCommon Lispには、ユニークな名前のsymbolを返す関数 gensym があります。 ( eq 'hoge 'hoge ) ; T ( eq ( gensym ) ( gensym )) ; NIL 1つ目のプログラムでは、2つのhogeというsymbolを比較し、真であることを表す T というsymbolが返っていますが、2つ目のプログラムでは、 (gensym) の返す2つのsymbolを比較し、偽であることを表す NIL というsymbolが返っています。 (gensym) もJavaScriptの Symbol() と同様に名前衝突を回避する目的で使用されます。 Lispでは頻繁にマクロと呼ばれるプログラムを変更するプログラムを書きますが、マクロ定義の中で gensym を使うことによってマクロ展開後のプログラム中で名前が衝突することを回避できます。 以下では OnLisp から for マクロを紹介します。 ( for ( x 1 5 ) ( princ x )) ; 12345 for マクロは一般的な手続き型言語の for のようにループを記述できる便利なマクロです。 for マクロはCommon Lisp組み込みの do マクロをラップすることで以下のように定義できます。 ( defmacro for (( var start stop ) & body body ) ( let (( gstop ( gensym ))) ` ( do (( , var , start ( 1 + , var )) ( , gstop , stop )) (( > , var , gstop )) ,@ body ))) for マクロの引数は (var start stop) と body です。 var にはループ中で使用するループ変数、 start にはループ変数の初期値、 stop にはループ終了判定の際にループ変数と比較する値を渡します。 body にはループ中で繰り返し実行されるプログラムを渡します。 for マクロの展開後のプログラムは (do ...) の部分ですが、その前にローカル変数 gstop に (gensym) の返すユニークなsymbolを束縛しているところがポイントです。 この for マクロのみを展開した結果のプログラムを macroexpand-1 を使って見てみます。 ( macroexpand-1 ' ( for ( x 1 5 ) ( princ x ))) ; (DO ((X 1 (1+ X)) (G2951 5)) ((> X G2951)) (PRINC X)) for マクロに渡した引数が、 do マクロにきちんと渡っていることが確認できます。 G2951 という名前のsymbolは (gensym) が返したsymbolです。今回ユーザが for マクロに渡したループ変数としてのsymbolは x でしたが、どんな名前のsymbolが渡されたとしても名前衝突は発生しません。 名前衝突を利用したLispプログラム(アナフォリックマクロ) ここまで、JavaScriptの Symbol() とCommon Lispの (gensym) が名前衝突の回避に使用される例を紹介しました。 最後に名前衝突を利用したおもしろいCommon Lispプログラムを、こちらも OnLisp から紹介したいと思います。 次の例はある計算結果が nil でなければ、それを関数 foo に渡すというプログラムです。 ( let (( result ( big-long-calculation ))) ( if result ( foo result ))) このプログラムが次のように記述できれば楽ちんです。 ( aif ( big-long-calculation ) ( foo it )) 計算結果が勝手に it というsymbolに束縛され、then節で参照できています。 この便利な aif マクロは次のように定義できます。 ( defmacro aif ( test-form then-form & optional else-form ) ` ( let (( it , test-form )) ( if it , then-form , else-form ))) aif の第1引数のtest節の計算結果を it に束縛し、ifでテストしています。マクロ展開後のプログラムを見てみると意図した通りのプログラムに変換されていることが確認できます。 ( macroexpand-1 ' ( aif ( big-long-calculation ) ( foo it ))) ; (LET ((IT (BIG-LONG-CALCULATION))) (IF IT (FOO IT) NIL)) マクロに渡すthen節中で it という名前のsymbolを使用することによって、展開後のプログラムで名前を衝突させています。このように名前を衝突させて、symbolに代名詞的な働きをさせるマクロはアナフォリックマクロと呼ばれます。 おわりに 最後まで読んでいただき、ありがとうございました! この記事ではJavaScriptの Symbol() や、Common Lispの (gensym) が返すユニークな名前を持つsymbolを使って、名前衝突を回避するプログラムを紹介しました。また、意図的に名前を衝突させるプログラムも紹介しました。古い言語の機能や工夫が、新しい言語に取り入れられる様子を見る度に、「昔コレ考えた人すごいなぁ」と思います。 私はこの記事を執筆するにあたって久しぶりにLispを書きました。楽しかったので今週末も少し書いてみようかなと思っているところです。
これは、FORCIA Advent Calendar 2021の11日目の記事です。 こんにちは! 旅行プラットフォーム部エンジニアの恒川です。 今年10月に入社し、毎日JavaScriptを書いています。 この記事では、JavaScriptのsymbolから始めて、「名前衝突」をキーワードに、それを利用したLispプログラムまで紹介したいと思います。 JavaScriptのsymbol symbolはES2015で追加されたプリミティブです。プリミティブとはメソッドを持たないデータのことで、42、"Brendan Eich"などの仲間です。 symbol型のデータは関数Symbol
これは、 FORCIA Advent Calendar 2021 の10日目の記事です。 営業3年目の松浪と申します。 今回アドベントカレンダーにて一枠いただくことになり、何を書くか迷っていたら締切になりました。(他の会社の営業職の方々がどのようにしてテーマを選んでいるのか知りたいです。。) 追い込まれた結果、システム会社の営業(非エンジニア)職を志望している就活生によく聞かれるので「非エンジニアならプログラミングよりもシステムについて(というかそれらの違いを)知ろう」という思いから書き始めることにしました。 ITを使ったビジネスにはプログラミング以外にも必要なことが多岐にわたるので、個人の考えではありますが、少しでもIT系の企業に営業職として就職する・したい方の参考になれば幸いです。 非エンジニアでもプログラミングできた方がいいですか? 営業職を志望される方によく聞かれることとして「プログラミングができた方がいいですか?」という質問がよくあります。 もちろん、プログラミングが出来た方がいいです。 が、個人的にはプログラミングではなく システムがどう動いているのかの知識をつける方が圧倒的に優先順位が高い と考えています。 プログラミングは「データをどう処理するかを命令するためのコーディング」で、システムは「処理されるデータやその処理を動かす箱」であり、普段閲覧しているWebサイトの裏側ではどういう仕組みがあって利用できているのかを知ることがシステム知識をつけるということだと考えています。 システムって何かよく分からないという方には、フォルシアで新卒入社する際には必ず配られる下記書籍をおすすめします。 「プロになるためのWeb技術入門」 ――なぜ、あなたはWebシステムを開発できないのか (小森裕介 著/技術評論社) WebやシステムといういわゆるITと呼ばれるものを理解するのにとても助けになり、一通り読むことでシステムに対する概観をつかめると思います。 非エンジニアはどこまで技術を知ればいいのか? フォルシアは「検索」を主軸としているため、エンジニアだけが「検索」について知っていればいいわけではなく、営業職も勉強すべきということは言うまでもないかと思います。 ただ、こういった技術的な話は深すぎて、どこまで学べばいいの??となってしまいます。 自分の場合は、「どうやって実現するか」はエンジニアにお任せし、「どうなるのが望ましいか」を考えることに努めています。 具体的には「いい○○」や「理想の○○」(※○○には「検索」や「プロジェクトマネジメント」など色々入ります)を知ることに主眼を置いています。 例えば、こちらの記事を参考に「検索」を学びました。 「いい検索」とはなにか?検索システムのしくみと評価指標を解説 (ログミーTech) 自分は非エンジニア職のため実装するわけではないですが、検索の基本的な考え方(どういった処理がシステム側で行われるか・行うべきか)を知るとエンジニアの方々と話せる内容の質は高くなるのでオススメです。 上記のようなシステムや技術概要、その他UI/UXや仕事の方法など、プログラミングよりも優先して知っておいた方がいいことは多くあるので、以下習慣的に閲覧しているWebサイトとオススメ書籍を紹介して締めくくろうと思います。 習慣的に閲覧しているWebサイト 他にも数多く存在するので、おすすめのサイトがあればぜひご教示願いたいです。 テッククランチ サービスというよりはビジネススキームの話かなと思いますが、たまに着想を得られます。 Bridge スタートアップ系の記事が多く、ビジネスアイデアを考える上で参考にしています。 Bloomberg 金融系の情報のため自身の業務に直接参考になることはほとんどないですが、ビジネスマンっぽいので嗜んでいます。 Books&Apps 独自の考え方や解釈が掲載されるメディアで、毎日マインドセットをさせていただいています。 ログミー セミナー内容を記事にしてくれており立体的な情報を得られますが、長いので大体流し読みになってしまいます。 biz, tech 両方お世話になっています。 NIJIBOX UX系のノウハウを分かりやすくまとめてくれていて、自分の行動に少しずつ取り込んでいっています。 以前は「 UX MILK 」を読んでいました。 スタタイ スタートアップについてリッチな情報が掲載されており、新規サービスや機能を考える際にお世話になっています。 その他面白かった書籍 フォルシアの営業職は営業・契約・要件定義・開発進捗管理など幅広く担当するので、いずれかに特化した内容の紹介ではなく面白かった5つの書籍を記載します。 どれも実践が難しいのですが、だからこそ何度も読み直し、少しずつでも洗練していければと考えています。 大型商談を成約に導く「SPIN」営業術 (ニール・ラッカム 著/海と月社) ノウハウというよりは営業を体系的に知るためにいい本だなと思いました。 センスメイキング (クリスチャン・マスビアウ 著/プレジデント社) 事実を点で見るのではなく、文脈を理解する大切さについて啓蒙されました。 システムを構築する・改善する際に、確実に助けになります。 心理的安全性のつくりかた (石井遼介 著/日本能率協会マネジメントセンター) 元々チームという概念が自分の辞書になかったのですが、プロジェクト(というかチームの関係性)が悪化した時に読みました。 「学習する組織」が最近のテーマになっています。 HARD THINGS 答えがない難問と困難にきみはどう立ち向かうか (ベン・ホロウィッツ 著/日経BP) ベン・ホロウィッツがかっこいいです。 「きついな、、」と感じた時に思考を転換してくれます。 ロジカル・シンキング 論理的な思考と構成のスキル (照屋華子、岡田恵子 著/東洋経済新報社) ビジネスコミュニケーションの土台を学べると思います。 最後に 結果的に自分の趣味・嗜好がかなり出そうで恥ずかしい記事になってしまいましたが、少しでも似たような性質の方の参考になれば幸いです。 念のためですが、フォルシアでは営業職もプログラミング研修を受けており、プログラミングができるほどエンジニアとの会話が容易になる面はあると思うので不要と言いたいわけではなく、あくまで優先順位としては上記のような内容が先に来るという主張になります。 インプットしても実践で使えなければ何の役にも立たないですが、新しい情報を得ることでチャレンジしようと思えたり、着想を得られることが多いので、これからも継続していこうと思います。 (余談ですが、インプット:アウトプット=3:7という割合が成長する配分として丁度いいと同期が話していました。) フォルシアの営業は若手の内から現場に出てディレクションできるので、アウトプットの場が豊富ですし、自分次第でいくらでもチャレンジできる環境があると思います。 そのような環境は、自分の至らなさを許容してくださるお客様の懐の大きさやその関係性を築いてくれた先輩方という状況があってのものなので、感謝で締めくくれればと思います。
これは、 FORCIA Advent Calendar 2021 の9日目の記事です。 こんにちは、第2旅行プラットフォーム部エンジニアの力石です。 近頃、AIが様々な分野で活躍する話を以前に増してよく聞くようになりました。 個人的にその中でも興味深いのは、AIが漫画や絵を創作するといった芸術分野やエンターテインメント分野への応用です。 こういったAIによる絵の創作では、画像をぼかしたり特定の色を抽出するような画像処理の技術や機械学習、深層学習などの技術が使われています。 今回はそれらの技術の一部を使ってみて、簡単な有彩色、無彩色の分類をしてみたいと思います。 そもそも有彩色、無彩色とは? 一般的に有彩色、無彩色の定義は以下だと考えられます。 有彩色:すこしでも色味のある色 無彩色:色味のない色 例を上げると、赤色や青色などは有彩色、灰色や黒色は無彩色となります。 有彩色、無彩色のパラメタとしては「色相、明度、彩度」があります。これはそれぞれ色味、明るさ、鮮やかさを表します。 なにを基準に分類するのか 定義から有彩色、無彩色の分類には「色相、明度、彩度」のパラメタを使えば良さそうです。 しかし今回は液晶ディスプレイなどで使われているRGB値で分類をしてみたいと思います。(RGB値とは、赤、緑、青の3つの原色からさまざまな色を表現する方法です) 理由としては、RGB値と有彩色、無彩色の関係がどうなっているか追求してみることにロマンを感じたからです。 RGB値で有彩色、無彩色を分類するにあたり、仮説をたててみます。 次の図では、左は有彩色と考えられる色の、右は無彩色と考えられる色のRGB値を示しています。 この図を見た感じ、左はRGB値がまばらになっており、右はRGB値がほとんど同じように思えます。 そこで、「RGB値それぞれの値の差の絶対値の合計が小さいほど無彩色である可能性が高く、大きいほど有彩色である可能性が高い」という仮説をたて、それに基づき分類してみたいと思います。 分類方法とその実装 ここでは以下の環境で実装しています。 Python : 3.6.3 opencv-python : 3.4.2.17 NumPy : 1.18.5 scikit-learn : 0.21.3 まずはOpenCVで画像を読み込みます。 OpenCVではデフォルトでBGRの順で画像を読み込むため、別ライブラリで画像を出力する際には注意が必要です。(matplotlibなど) 今回はOpenCVで出力するため、このままにします。 import cv2 # 画像のパス read_path = "image_file_to_path" # bgrで読み込み img = cv2.imread(read_path) 次に減色処理をします。 一般にRGB値はそれぞれ0~255まであるため、組み合わせは256の乗の約1600万となります。 以降の処理を簡単にする、ある程度決まった色の数から分類してみるといった理由から、同系統の色をまとめる減色処理を行います。 何色まで減らせば良いなどの目安はないため、決め打ちで64色まで減らしてみます。 なお、以下の実装は OpenCV-Python チュートリアル を参考にしました。 # 減色処理 decrese_num = 64 # 何色まで減らすか img = img.reshape((-1,3)) # 画像のサイズ変更 float_img = np.float32(img) # k-mmeansのためにfloatへ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) # k-meansの終了条件 _, _, center = cv2.kmeans(float_img, decrese_num, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS) # k-meansの実行 最後に有彩色、無彩色の分類をします。 先程の仮説に基づき、64色それぞれのRGB値の差の絶対値の合計(以下、スコアと呼びます)を求めます。 そしてスコアを特徴ベクトルとし、k-means法でクラスタ数2のクラスタリングを行います。 クラスタリング結果と先ほどの減色処理の結果である64色のRGB値の重心(center)から、64色のうち無彩色と分類されたRGB値、有彩色と分類されたRGB値がわかります。 # 有彩色、無彩色の分類 score_array = [] # 分類スコアの格納 ## 各色のスコアを算出する for i in range(decrese_num): score = abs(center[i][0] - center[i][1]) + abs(center[i][0] - center[i][2]) + abs(center[i][1] - center[i][2]) # スコアの算出 score_array.append([score]) color_class = KMeans(n_clusters=2).fit(score_array) # クラスタ数2でクラスタリング ## 無彩色と分類されたクラスタを抽出する min_label = 1 if color_class.cluster_centers_[0][0] > color_class.cluster_centers_[1][0] else 0 non_colored_index = np.where(color_class.labels_ == min_label)[0] ## 有彩色と分類されたクラスタを抽出する colored_index = [i for i in range(decrese_num) if i not in non_colored_index] 分類結果と感想 下の図が2枚の画像を分類した結果になります。 使用されている色の種類が少なそうな画像(上の段)と多そうな画像(下の段)を今回は選んでみました。 どちらも元画像と減色処理後の画像に大きな変化は見られないですね。 上の段の有彩色の画像を見ると、有彩色らしい色が分類されています。しかし、元画像には殆ど見られない緑色がある点が気になります。 これは減色処理の際に数ピクセルだけ緑色に変換され、それが有彩色と判断されたためだと考えられます。 下の段の有彩色の画像でも概ね有彩色らしい色が分類されていると思われます。 上の段の無彩色の画像を見ると、全体的に暗めな色や白色が分類されています。 少し明るめの肌色がある点は気になりますが、概ね無彩色だと言える色です。また、似たような色が多いため、元画像で使用されている色の種類も少ないと考えられます。 しかし、下の段の無彩色の画像では明るめの色がいくつか見られます。 以上より今回の手法では、色の種類が少ない画像の有彩色、無彩色の分類はできると考えられますが、さまざまな色を使用した画像の有彩色、無彩色の分類は難しいことがわかりました。(とはいえ、2枚の画像で試しただけですので、たまたまこうなった可能性も考えられます) このような結果になったのは、今回の手法では特徴ベクトルが1種類でクラスタ数が2という、超シンプルなクラスタリングなためだと考えられます。どのような特徴ベクトルを追加すればよいか、そもそも仮説が間違っているのか、よければ考えてみてください。 最後に 今回は簡単な有彩色、無彩色の分類をしてみました。 このような簡単な分類でも苦戦している筆者からしたら、最初にお話ししたAIで絵の創作といったプロジェクトの研究、開発をしている方々には尊敬の念を抱かずにはいられません。 また、明日以降も FORCIA Advent Calendar 2021 の記事が公開されますので、ぜひご覧ください。
画像中のRGB値から有彩色、無彩色を分類してみる これは、FORCIA Advent Calendar 2021の9日目の記事です。 こんにちは、第2旅行プラットフォーム部エンジニアの力石です。 近頃、AIが様々な分野で活躍する話を以前に増してよく聞くようになりました。 個人的にその中でも興味深いのは、AIが漫画や絵を創作するといった芸術分野やエンターテインメント分野への応用です。 こういったAIによる絵の創作では、画像をぼかしたり特定の色を抽出するような画像処理の技術や機械学習、深層学習などの技術が使われています。 今回はそれらの技術の一部を使ってみて、簡単な有彩色、無彩色の分類を
これは、 FORCIA Advent Calendar 2021 の7日目の記事です。 こんにちは。エンジニアの長尾と申します。旅行系アプリの開発・運用をしています。 今年の秋頃からSQLの高速化に取り組み、計600分以上の短縮に成功しました。 そのなかで特に効果の大きかった施策を4つほどご紹介させていただきます。 1. たった25行のコードの追加で、165分短縮。 「このTSVファイル20Gを超えているけど、一体これは何なんだ......。」 これを見たときに感じたことは「このデータは本当に全部使われているのだろうか」ということでした。 そこで、そのTSVファイルをもとに作られたテーブルがどこでどう使われているか調査しました。 いろいろな箇所でいろいろな使われ方をしていましたが、ひとつ共通点がありました。 必ず特定のテーブルとINNER JOINされている。 SELECT hoge FROM 巨大テーブル INNER JOIN とあるテーブル そしてJOIN後のレコード数は1/10以下になっていました。 と、いうことは90%以上のレコードは全く使われていなかったのです。 そこで、最上流である元データからTSVを生成する箇所で、そのテーブルをINNER JOINしました。 すると、20Gあった巨大TSVファイルは2G程度になり、関連する全ての処理が高速化しました。 それだけではありません。 DBのダンプ、転送、DBのリストアなども高速化し、計165分の大幅な短縮となりました。 まとめ:不要なレコードは最上流で削ろう 2. 100分かかっていたプレウォームを15分に。 アプリケーションによってはプレウォームを実施していることがあるかと思います。 これは、いろいろなSQLを前もって投げておくことで、 キャッシュをためてオンラインでのパフォーマンスを上げることが目的です。 このプレウォームに時間がかかっていたので原因を調査しました。 そして、SQLをいじって試行錯誤しているときに、あることを発見しました。 WHERE あるパラメータ = hoge ある一つのパラメータをWHERE句から消すと超高速になったのです。 「このパラメータ消したい」 しかし、当然ながら必要だから書かれているわけで、消すわけにはいきません。 どうしよう......。消したいけど消せない......。消せないけど消したい......。 あ! 嗚呼! テーブルのほうを分ければいいんだ。 そのパラメータの取り得る値は、1〜10といった具合に範囲が決まっており、 さらにユーザーは必ずその一つを選ぶ(必須かつ単数)という形式だったのです。 もとのテーブル → 新テーブル1, 新テーブル2, 新テーブル3 ...... 新テーブル10 と、分割し、レコードもそれぞれの新テーブルに分割して登録しました。 これにより晴れてWHERE句から消すことができたのです。 旧クエリ SELECT hoge FROM もとのテーブル WHERE あるパラメータ = '2'; 新クエリ SELECT hoge FROM 新テーブル2 WHERE あるパラメータはもうない; プレウォームが早くなっただけでなく関係するオンラインクエリもおよそ5倍速になりました。 まとめ:必須かつ単数のパラメータの処理が重くて困ったら、テーブルを分割したら解決するかも 3. ループにご注意。何度も繰り返されるSQL。 こんなSQLがありました。 5回ループ { CREATE TABLE 新テーブル_{1~5} AS SELECT hoge FROM Aテーブル INNER JOIN Bテーブル INNER JOIN Cテーブル INNER JOIN Dテーブル INNER JOIN Eテーブル_{1~5} ; } Eテーブルが1〜5で分かれていて、それに対応して新テーブルも5つに分かれます。 どうにかして計算量を減らしたい。さてどうしたものか。 ......。 CREATE TEMPORARY TABLE 一時テーブル AS SELECT hoge FROM Aテーブル INNER JOIN Bテーブル INNER JOIN Cテーブル INNER JOIN Dテーブル ; 5回ループ { CREATE TABLE 新テーブル_{1~5} AS SELECT hoge FROM 一時テーブル INNER JOIN Eテーブル_{1~5} ; } こうですね。 これで、BCDテーブルをAテーブルにJOINする回数が1回で済みました。 これだけで1/5強の処理時間になりました。 これは簡単なことですが、案外やってしまいがちだと思います。 そして、ちゃんと対応したときの効果は絶大です。 面倒くさがらずに一時テーブルを作ろう、というお話でした。 まとめ:ループを書くときは、本当にループさせるべきもの以外は外に出す 4. 超難解なSQLでも諦めないで。高速化できます! この勢いでどんどん高速化してやろう、と意気揚々、次なるターゲット(遅いSQL)を眺めていました。 が、分からないのです。 何が書いてあるのか、全然。 SELECT文のなかで多重ループがされていて、そのループのなかでも関数がガンガン呼ばれていて......。 しかもSELECT自体も多階層になっていて、何が何やらという状態でした。 かろうじて階層の一番深い所の処理だけは理解できました。 SELECT 超複雑な処理 FROM ( SELECT 超複雑な処理 FROM ( SELECT 超複雑な処理 FROM ( -- かろうじて理解できた部分、ここから SELECT hoge FROM Aテーブル INNER JOIN Bテーブル INNER JOIN Cテーブル INNER JOIN Dテーブル INNER JOIN Eテーブル -- かろうじて理解できた部分、ここまで ) ) ); こんなSQLです。 しかも悲しいことには、時間がかかっている処理は、まさにその超複雑な処理の部分なのです。 さすがにこれは高速化は無理だよなぁ、と一度は諦めました。 が、しかし、口惜しい。 このSQLを見るたびに、どうにかしてやっつけられないだろうか、とずっと思っていました。 そんなあるとき、ほかのSQLで、一時テーブルをANALYZEしているものを見掛けました。 一時テーブルのためにわざわざANALYZEなんかして、一体何をやっているんだ......。 あ! これだ! 最深部のSQLを一時テーブル化して、ANALYZEした状態で超複雑な処理を迎えれば......。 CREATE TEMPORARY TABLE 一時テーブル AS SELECT hoge FROM Aテーブル INNER JOIN Bテーブル INNER JOIN Cテーブル INNER JOIN Dテーブル INNER JOIN Eテーブル ; ANALYZE 一時テーブル; SELECT 超複雑な処理 FROM ( SELECT 超複雑な処理 FROM ( SELECT 超複雑な処理 FROM 一時テーブル ) ); なんとこれだけで25%の高速化に成功しました。 まとめ:困ったときのANALYZE さいごに やってみると意外と改善の余地はたくさんあるものだなぁと思いました。 SQLが速くなると気持ちが良いですね。 最後に各項のまとめを再掲しておきます。 不要なレコードは最上流で削ろう 必須かつ単数のパラメータの処理が重くて困ったら、テーブルを分割したら解決するかも ループを書くときは、本当にループさせるべきもの以外は外に出す 困ったときのANALYZE
こんにちは。エンジニアの長尾と申します。旅行系アプリの開発・運用をしています。 今年の秋頃からSQLの高速化に取り組み、計600分以上の短縮に成功しました。 そのなかで特に効果の大きかった施策を4つほどご紹介させていただきます。 1. たった25行のコードの追加で、165分短縮。 「このTSVファイル20Gを超えているけど、一体これは何なんだ……。」 これを見たときに感じたことは「このデータは本当に全部使われているのだろうか」ということでした。 そこで、そのTSVファイルをもとに作られたテーブルがどこでどう使われているか調査しました。 いろいろな箇所でいろいろな使われ方をしていました
これは、 FORCIA Advent Calendar 2021 の6日目の記事です。 こんにちは!2021年入社、新卒1年目エンジニアの小木曽です。 大学では主に言語学を学んでいましたが、色々な経緯があり技術職としてフォルシアに入社しました。 大学の専攻とはかなり違う領域の職種に就いたこともあり、周りからは「珍しいね」と言われることも多いのですが、学生時代の後輩たちと話していると、文系からIT技術職への就職を検討している人は意外と少なくないことが分かりました。 というわけで、この記事では自分の学生時代から現在までを振り返りつつ、フォルシアのエンジニアになっての所感をお伝えできればと思います。 内容は大きく分けて以下の3つです。 大学前半:エンジニアを目指した経緯 大学後半:フォルシアを志望した理由 現在:実際にエンジニアになってみて 同じような進路を検討している就活生の参考になれば幸いです。 1. 大学前半:エンジニアを目指した経緯 私は、最初からずっとエンジニアを志望していたわけではありませんでした。大学1年生の頃は、ただ漠然と専攻分野に関係する仕事に就ければ良いかと考えていたのですが、2年生の夏にとある大手企業でインターンに参加した際、初めて「自分の好きなことを活かせる仕事がしたい」と感じ、将来について能動的に考えるようになりました。 私は昔から絵や音楽、ダンスの振付などの創作活動が好きでした。こういう"何かを作ること"と"社会で必要とされていること"を掛け合わせたらどうなるか考えた時、「プログラミングを学んでエンジニアになる」というところに帰着したのです。その他にも、SEをやっていた親の影響や性格分析の結果に出た向いている職業欄に技術職があったことなどいろいろな要因があり、エンジニアを目指す気持ちが高まりました。 とはいえこの時のプログラミング経験は皆無。大学では情報系の授業がほとんどなく、独学で学ぶのも不安があったため、まずは3年生からプログラミングスクールに通い始めました。ここでは、プログラミングの基礎の基礎を勉強し、成果物として簡単なwebアプリケーションを作ったのですが、自分のアイデアを技術の力で形にすることの楽しさを知ることが出来ました。 スクールを卒業すると、成果物を持ち込みながら実務として開発が出来るインターン先を探し始めました。実務経験がある方が就活をする上でも熱意が伝わりやすいと思ったからです。 文系未経験ながら拾ってくれた会社では、クリエイティブ業界向けのSaaS開発に携わりました。ここでは、個人開発では得られなかった、チームで一つのプロダクトを作る難しさや楽しさを知ることが出来ました。コーディングなどの直接的な技術はもちろんのこと、周りのエンジニアや営業の方とスムーズにプロジェクトを遂行するためのコミュニケーション能力も必要とされていることを実感しました。 2. 大学後半:フォルシアを志望した理由 フォルシアを知ったきっかけは、就活の相談をしていたエージェントでした。「社風が合うのではないか」と紹介され、実際に社員の方と直接何回か話をしました。どなたも親切で、一就活生の拙い質問にも丁寧に答えてくれました。 特に印象的だったのが、非情報系出身で当時2年目のエンジニアの方と話したときのことでした。プログラミングはほぼ未経験の状態から入社したそうですが、充実した教育体制が整っていたことやいつでも先輩に相談できる環境のおかげで業務をこなせるレベルにまで成長できたとのことでした。この話を聞いて、フォルシアの人の温かさや教育にかけるコストを惜しまない風土に魅力を感じ、志望度が高まりました。 また、フォルシアのエンジニアの特徴として、「案件の提案、要件定義から、設計、実装、テスト、運用までを一貫して担当する」というのがあり、「目の前のコードと向き合うだけでなく、コードの先にいるヒトと向き合っていきたい」と考えていた私にとっては働き方という点でも魅力を感じ、この会社でエンジニアとして働きたいと思うようになりました。 いざ選考フローに入ると、履歴書を見ながら「なぜ言語学を専攻していたあなたがここに...?」ということは当然のように毎回聞かれます。そのような時は1節で述べたようなことを説明していたのですが、社長・COOとの最終面接で「今大学で学んでいることとこれからやりたいことを結びつけて話してください」という質問を受けた時には少し悩みました。 緊張で記憶がおぼろげではありますが、「言葉も技術も今では人間が社会を維持していくのに欠かせないもの。生活の中で当たり前の存在でありながら実態の無いものに関わり、支えていきたいという志向を持っているので、このような進路を選んだ」という旨を話しました。 今振り返れば少しこじつけのような気がしなくもないですが、とにかくエンジニアに興味があって、フォルシアという環境で貪欲に学んでいきたい!いう気持ちはアピール出来たと思っています。 大学で学んでいたことと異なる道を選ぶ人は、その道を選ぶに至ったストーリーや過去と未来を結ぶ共通項を、相手に納得してもらえる形で言語化する練習をしておくと良いかもしれません。 3. 現在:実際にエンジニアになってみて そんなこんなで実際にフォルシアで技術職として採用されて今日に至ります。現在はECサイトの検索機能を担当するチームにJOINして日々開発中です。実際にフォルシアのエンジニアになってみて、入社前に社員の方から聞いた内容がその通りだったということを実感しています。 技術研修では、Progateに始まり、JavaScript・SQL・検索アプリ開発のオリジナル教材を進めていくことで、フォルシアのエンジニアになるために必要な基礎知識が自然と入ってきました。やはり情報系専攻の優秀な同期と比べると能力や経験面での差は大きく、焦ることも多々ありましたが、メンター制度のおかげで先輩に気軽に相談・質問することが出来るので、不安や疑問をその日のうちに解消できたのは有り難かったです。 それから仮配属を経て本配属となるのですが、ここでも分からないことはすぐ解決できる体制が整っていると感じました。Slackやesaなど社内知見を溜め込めるツールのおかげで、大抵の問題は検索すると似たようなケースに対処した先人の知恵を享受できたからです。 実務だけでは足りない・もっと知りたい部分については会社の書籍補助制度を利用して技術書を読んでみたり、資格(基本情報技術者試験)取得に向けて勉強したり、ドットインストールなどの外部サービスを使って学習したりしています。 研修のときと比べると、周りと比べ過ぎず自分のペースで頑張っていこうと気持ちを切り替えたことで精神的にも少し落ち着いてきた感覚がありました。 今後は、興味のあるフロントエンドやデザイン・UIUX領域にも携わりたいと思っており、業務の中で学べそうな部分を積極的に吸収したいと考えています。 おわりに もし「文系からでもエンジニアになれるのでしょうか?」と聞かれたら、私は「なれます!」と答えたいです。フォルシアの教育体制はとても充実しているので、志さえあればどんなことを学んできた方でも受け入れる環境となっています。 大事なのは「エンジニアになって何を生み出していきたいのか」という部分だと思っています。これは選考の過程から聞かれる部分でもあるので、非情報系出身なら特にしっかり考えを磨いておく必要があると思います。ここがはっきりしていると、会社という環境を生かして自己実現するスピードも早まっていくというメリットもあるので、ぜひ意識してほしいポイントです。かく言う私も日々考え直している点であります。 実際にエンジニアになってみて感じたのは、「どういう職種に就くかではなく、就いた仕事でいかに自分のバリューを発揮していくかの方が重要だということ」です。たとえ全く異なる専攻出身だったとしても、仕事に生かせることはあると思います。 私の場合、大学では様々な言語を学んできましたが、「基本的な単語や文法を覚えていく→覚えたものを自分で使ってみて習得する」ということをひたすら反復する学習の流れは、自然言語でもプログラミング言語でも共通するところがあると感じています。 また、文章やビジュアルを用いて情報を整理することが好きという志向を生かし、営業の方との細かい仕様確認のときに分かりやすく伝えることを心がけたり、社内に残すドキュメントの体裁を整えたりすることも楽しんでやっています。 そういう意味で、フォルシアの一気通貫した開発スタイルは情報系以外を専攻してきた方にも得意なこと・好きなことを活かせる場になり得ると思うのです。 最後に、働く上で私が大切にしている言葉に「自分が選び取った行動が正解になるように努力を続けることが『成功の秘訣』」というものがあります。これは、就活生の頃初めてフォルシアへ行った時に、人事の方からもらった社長執筆の本にあった内容です。 確かに、学生時代とは全く異なる環境では不安や悩みも尽きません。大学の同期が言語・教育関連の仕事で活躍している話を聞くと、本当にこの道を選んで良かったのかと自問することもあります。 しかし、今出来ることは「目の前の道を最善とするために日々努力すること」だけだと思っています。まだまだ自分一人の力では出来ないことが多いですが、サポートしてもらえる環境・先輩方に感謝し、その分昨日より少しでも分かったこと・出来るようになったことを増やせるよう意識しています。 もし、この記事を読んで「フォルシアのエンジニア」という道に興味を持ってくださった方がいればぜひ採用応募フォームからご連絡ください。 フォルシアという環境で、自分ならではのエンジニア像を一緒に実現させていきませんか?
はじめに こんにちは!2021年入社、新卒1年目エンジニアの小木曽です。 大学では主に言語学を学んでいましたが、色々な経緯があり技術職としてフォルシアに入社しました。 大学の専攻とはかなり違う領域の職種に就いたこともあり、周りからは「珍しいね」と言われることも多いのですが、学生時代の後輩たちと話していると、文系からIT技術職への就職を検討している人は意外と少なくないことが分かりました。 というわけで、この記事では自分の学生時代から現在までを振り返りつつ、フォルシアのエンジニアになっての所感をお伝えできればと思います。 内容は大きく分けて以下の3つです。 大学前半:エンジニアを目指し
これは、 FORCIA Advent Calendar 2021 の5日目の記事です。 こんにちは。プロダクト部 技術研究所の大沢です。 私が大学生のころ、Nintendo DSのアソビ大全というゲームソフトに定番ゲームの1つとして収録されていた「ラストワン」というゲームがあり、必勝法がないものか、あるとしたらどうにか導けないか?と気になっていた時期がありました。何週間か考えていましたが、当時はプログラミングも学びたてで、結局必勝法を編み出すことはできませんでした。 つい最近になってそんなゲームがあったことを思い出し、ふと考え直してみたところ、なんと必勝法を編み出すことができました! その解法には、私がここ数年腕を磨いている、競技プログラミングで得た知識が生きていると感じました。 10数年の時を超えてゲームを攻略した喜びを共有したく、この記事を書くことにしました。 ラストワンというゲームについて ルールは単純です。もしかしたら皆さんも紙や黒板で遊んだことがあるかもしれません。 まず、初期状態として縦棒を並べて書きます。 ピラミッド型じゃなくても良いですが、とにかく横に何個か縦棒を並べたものを何行か書きます。 次に、じゃんけんか何かで、先攻か後攻かを決めます。 ゲームは先攻と後攻で順番を交代しながら進みます。 自分の番の人は、まだ消されていない縦棒を選び、横線で消します。 このとき、まだ消されていない縦棒が横に連続していれば、複数まとめて消すこともできます。 すでに消された縦棒をまたいで消したり、違う行の縦棒をまとめて消すことはできません。 このように順番に縦棒を消していき、 最後に残った1本を消した人の負け です。 なお、Android端末では こちら より実際に遊ぶことができます!(筆者作のアプリです) 必勝法とは? 実はこのゲームはすべての局面が「勝ち」か「負け」かが決まっています。 負けの局面について 例えばこの状態で手番が回ってくると、負け確定もしくは、相手が間違えない限り負けてしまいます。 勝ちの局面について 例えばこの状態で手番が回ってくると、勝ち確定もしくは、自分が間違えない限り勝つことができます。 突き詰めると、初期状態で先攻か後攻かを適切に選ぶゲームになります。 つまり必勝法を知っている人にとっては、初期状態が勝ちの局面であれば先攻を選び、負けの局面であれば後攻を選ぶことができれば必勝となります。 後退解析による解法 実は特殊な知識がなくても、プログラミングが得意な方であれば、すべての局面を勝ちと負けに分類できます。 後退解析という手法を使います (動的計画法の一種) ある局面Aからどれか1つでも負け局面に遷移できればAは勝ち局面といえます。 反対に、勝ち局面にしか遷移できなければAは負け局面といえます。 ゲームの終了状態での勝ち負けは決まっているので、ゲームの終了状態から遡っていくことで勝敗が分かります。 これをシミュレートするのが後退解析です。 局面を圧縮する 一般的に動的計画法で問題を解く際、無数に状態が存在するのを共通の特徴で同一視して、状態を減らすことを考えます。 このゲームでは、たとえば この2つの局面は同じとみなすことができます。どちらも、 (3,2,1,1) という数列で表現できます。 これは、1本の孤立した縦棒が3個、2本並んだ縦棒が2個、3本並んだ縦棒が1個、4本並んだ縦棒が1個 残っているという意味です。 メモ化再帰 この問題は再帰関数で実装するのが都合がよさそうです。 特に、再帰関数を実装する際、同じ状態の判定を2度行わないように結果をメモしておく手法をメモ化再帰と呼びます。 実装上は、状態を表す数列をキーにしてメモしたいので、hash化して使います。 Pythonなら、tuple型であれば勝手にhashを計算してくれますし、自前のhash関数を作っても構いません。 以下にPythonの実装例を記します。 memo = {} #判定結果をメモするdictionary def is_win_state(state): if state == (0,0,0,0,0,0): return True #最後の1本を消したら負け ⇒ すべて消された状態で回ってきたら勝ち if state in memo: return memo[state] #判定済みなのでメモの内容を返す result = False #get_next_states()は state から遷移可能なすべての状態たちを配列で返すメソッド(実装略) for next_state in get_next_states(state): if not is_win_state(next_state): #遷移先に負け局面が1つでもあれば、勝ちと判定する result = True break memo[state] = result #判定結果をメモする return result result = is_win_state((1,1,1,1,1,1)) #6段ピラミッド型の初期状態が勝ち状態であるか負け状態であるかわかる print(result) # True ちなみに先攻は勝ちのようです 後退解析を使うことで、あっさりとすべての局面が勝ちか負けかが求まってしまいました。 プログラミングで求めることはできましたが、残念ながらこのプログラムを頭の中で動かすことのできる人はそう居ないと思います。 (いくつかの局面の勝ち負けを暗記すればそれなりに強いですが...) どんな局面に対しても「計算」で、それも暗算可能な範囲で、求めることができたら、嬉しいと思いませんか? それが、次に紹介するGrundy数の方法です。 Grundy数を使った解法 ラストワンは 不偏ゲーム というジャンルに属するゲームです。 正確には、終了状態(terminal position)にした人が勝ちではなく負けになるので、「逆形の」不偏ゲームという分類になります。 不偏ゲームにはGrundy数の性質を使った必勝法があります。 Nimの例 (正規系不偏ゲーム) Nimという有名な不偏ゲームがあります。 初期状態で、石がいくつかの山になっています。 プレイヤーは自分の番の時にどれか1つの山から、1個以上好きなだけ取り除きます。 これを交互に行い、操作ができなくなった人の負け、つまり最後の1つを取った人の勝ちです。 ある局面のGrundy数を、「その局面から1手で遷移可能な局面のGrundy数のmex」で定めます。 ここで、mexとは、 非負整数の集合に対して、その集合に含まれない最小の非負整数と定義されます。 例) {0,1,2,3} のmex は 4 {0,1,3,4} のmex は 2 {1,2,3} のmex は 0 {} の mex は 0 特に、遷移不可能な局面 (terminal position) のGrundy数は、0です。 山ごとにGrundy数を考えます。次のように求まります。 まず、石が0個の状態は、遷移可能な局面がありませんので、Grundy数は0となります。 次に、石が1個の状態は、石が0個の状態にのみ遷移可能です。遷移可能なGrundy数の集合は{0}なので、これのmex、つまりGrundy数は1です。 次に、石が2個の状態は、石が0個の状態と石が1個の状態に遷移可能です。遷移可能なGrundy数の集合は{0,1}なので、これのmex、Grundy数は2です。 同様に、石が3個の状態は、遷移可能なGrundy数の集合は{0,1,2}なので、これのmex、Grundy数は3です。 ... すると、お気づきかと思いますが、結局NimにおけるGrundy数は、石の数に一致します。 Grundy数を求めると何が嬉しいかというと、 その局面の勝敗がわかります。0の局面は負け、0以外の局面は勝ちです。 このようにGrundy数を定めると、必ず0の局面(負け)から 0の局面(負け)に遷移させることは不可能であり、また必ず非0の局面(勝ち)からは0の局面(負け)に遷移させることが可能という性質を持ちます。 Grundy数にはもう1つ、嬉しい性質があります。 Grundy数同士は (bitwise) xor演算を使って合成できるのです。 本記事では(bitwise) xorの演算子として ⊕ を使います (bitwise) xorとは、ビットごとの排他的論理和です。 排他的論理和はビット演算の1つで、2つのビットのどちらか一方のみが1のときに限り、1となる演算です。 ビットごとの計算なので、2進数になおして、それぞれの位について計算します。 簡単にいえば、「繰り上がりのない足し算」です。 このxorの性質でGrundy数を合成していくことによって、複数ある山をあたかも1つの山とみなして局面を考えられるのがGrundy数の嬉しいところです。 Nimのセオリーをまとめると 山ごとにGrundy数を求めます (Nimでは山の石の個数と一致) それらGrundy数すべてをxor演算で合成した値を求めます 結果が0であれば負け局面、それ以外であれば勝ち局面です 合成したGrundy数が非0であれば、相手にGrundy数0の局面を押し付けられるので、自分の番で非0をキープでき、勝てます(下図参照 上図で(A)が成り立つ理由 Grundy数0の局面とは、各山の石の個数のxor結果が0ということ どこかの山から石を取り除くということは、どこかの山のどこかの位が最低1つは書き変わるということ するとxor結果が0から書き変わり、必ず0以外になる 上図で(B)が成り立つ理由 Grundy数が非0の局面とは、各山の石の個数のxor結果が0以外ということ (この値をxとする) xの最上位bit (2進数で最も上の位の1) に注目する xの最上位bitと同じ位が1である山が必ず1つは存在する (この山の石の個数をyとする) そうでなければ、xの最上位bitが1にはならず、矛盾する yの山を選んで、yをy⊕x個にできれば、xor結果を0にできる これは可能か?結論は可能 xの最上位bitが、yも1であるため、y⊕xにおける当該bitは0となり、y > y⊕xが成り立つ よって、yからy⊕xに「減らす」ことが可能 ラストワンに適用する 逆形不偏ゲーム (Misère Game) ですので、少し注意しなければなりませんが、Grundy数を適用できます。 Grundy数を適用する前に、自明なケース(下記の(1)(2)) から考えます。 (1) まず、1本の孤立した縦棒しか残っていないとき、勝敗は本数の偶奇で決まります 残り本数が奇数ならば負け、偶数ならば勝ちです。 (2) 次に、2本以上並んだ縦棒のかたまりが丁度1つあり、残りが全部1本で孤立しているケースは、勝ちの局面です なぜなら、1本の孤立した縦棒が奇数本ならば並んだ縦棒を全部消せば良く、偶数本ならば並んだ縦棒から1本残して消せば、残りが奇数本の①の局面を相手に渡せるからです。 (3) それ以外のケースをGrundy数を使って考えます Nimと同様に、かたまり毎にGrundy数を定義します。 便宜上、これ以上消せない状態(0本の状態)があるとして、このGrundy数を0とします。 これではNimと同じだから、「逆形」不偏ゲームのラストワンと勝敗が逆になってしまうのでは?と心配になるかと思いますが、大丈夫です。(理由はのちほど) では肝心のGrundy数の求め方なのですが、結論はNimと同じく、かたまりの本数に一致します。 確かに、 1本の状態からは0本にしか遷移できない (Grundy数:1) 2本の状態からは0本と1本に遷移できる (Grundy数:2) 3本の状態からは0本と1本と2本に遷移できる (Grundy数:3) ので、Nimと同じように考えることができそうです。 ですが勘の良い方は、ちょっと待った、と思うでしょう。 ラストワンでは、Nimとは異なる考慮も必要で、それは何かというと、かたまり(Nimでいう山)が増える遷移もあるということです。 3本のかたまりで真ん中の1本を消すと、左右に1本ずつの縦棒が残ります。この遷移は大丈夫なのでしょうか?結論は大丈夫です。 Grundy数の求め方は、「その局面から1手で遷移可能な局面のGrundy数のmex」でした。 さらに、Grundy数同士はxor演算で合成できます。 3本のかたまりで真ん中の1本を消し、1本の縦棒が2つ残った状態は1⊕1で、Grundy数は0です。 これを踏まえて、3本の状態をより正しく解釈するとこうなります。 3本の状態からは0本と1本と2本と「1本が2つ」に遷移できるが、「1本が2つ」のGrundy数は0なので、{0,1,2,0}のmexを求めることになり、Grundy数は3になります。 同じように、4本の状態からは0本と1本と2本と3本と「1本が2つ」と「1本と2本」に遷移できるが、「1本が2つ」のGrundy数は0、「1本と2本」のGrundy数は 1⊕2 で3なので、{0,1,2,3,0,3} の mexを求めることになり、Grundy数は4になります。 このように、ラストワンにおいても、n本のかたまりのGrundy数はnに一致することが示せます。(証明は省略します) ラストワンと勝敗が逆になってしまうのではないか?それが大丈夫な理由 この理由は、ラストワンでは③の状態からゲームが進むにつれて、必ず(2)の状態を経由して、次に(1)の状態を経由するからです。 特に(2)の状態のGrundy数は必ず非0となります。非0はNimでは勝ち局面でしたので、ラストワンの勝敗と一致します。 (2)の状態からはGrundy数を使わずに勝つことができますが、(2)から遷移すべき状態は(1)かつ残り奇数本の状態で、この状態のGrundy数は1です。 Nimでは非0の局面を相手に渡すと負けてしまいますが、ラストワンはこのケースならば勝ちです。 ここで勝敗がNimとは反対になるので、辻褄が合います。 ラストワンのセオリーをまとめると まず、(1)(2)(3)のどの状態であるか見極めます (1)ならば勝敗は決しています (2)ならば奇数本残るように①の状態にすると、勝ちます (3)ならば、Grundy数を使います Grundy数が非0ならば、相手にGrundy数0の局面を押し付けられるので、いずれ(2)の必勝局面が回ってきます。 Grundy数が0ならば、相手が間違えるのを待つしかありません。 初期状態が(3)のとき、先攻後攻を選べる状況ならば、Grundy数が非0ならば先攻、0ならば後攻を選ぶと良いです。 (おまけ) xorを暗算するときのコツ 先ほど暗算できると嬉しい... と書きましたが、実際xorの暗算は慣れていないと難しいです (笑) 2進数への変換は覚えるしかないのですが、少しだけコツを書き記します。 x⊕x = 0という大事な性質があり、同じ数のかたまりが2個あれば相殺されて0になります。 さらに加えて、 1⊕2⊕3 = 0 1⊕4⊕5 = 0 2⊕4⊕6 = 0 あたりを覚えておくとさらに計算を減らせることが出来て良い感じです。