くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログ。積極採用中です。

入社後の新卒研修ってどんなもの?今年はこんなことやりました!

こんにちは、ディレクターのめぐみです!

テクノロジー本部では現在採用に力を入れています。
去年からは新卒採用も始めたのですが、新卒の学生さんが会社選びをするにあたり、公開している情報が少ないってことに気付きました。なので入社したらまずどんなことをするの?そのイメージを持ってもらうべく、エンジニアの新卒研修について紹介します!

エンジニアの新卒研修、こんなことやっています!

テクノロジー本部では、1ヶ月ほどにわたり、みんなのマーケットで使っている技術や仕組みを知ってもらうための研修を行っています。

例として、

  • くらしのマーケットで使っている技術を使ってwebサービスを作る
  • ER図の更新をする

といったことをしています。

具体的には、下の6つのステップを踏んで、システム開発や設計について理解してもらいます。

  • 1.WEB技術の基本である、RESTやHTTPについて理解
  • 2.くらしのマーケットで利用されているmicro-serviceについて理解
  • 3.APIから必要な情報を取得するserverを構築
  • 4.HTTPの基本技術である、GETとPOSTを利用してWEBページとserver間での情報のやり取りを行いwebページに情報を出力するシステムを構築
  • 5.APIから取得した情報をredisを使って一定時間保存し、serverとAPI間の余分な通信を減らす技術を導入
  • 6.serverから非同期通信を利用して情報ごとに更新を行う技術を導入

私たちは「まずは実践してみること」を大切にしているので、実践しながら学べる研修内容になっています。 こうすることで技術面はもちろん、会社のカルチャーを理解してもらうのが狙いです。

新卒研修を受けた社員に感想を聞いてみました!

新卒のtakafumiくんに聞いてみました!

大学では情報系の勉強をしていましたが、今までwebに関することをほとんどやってきませんでした。 ただPythonを使うことには慣れていたので、web系の開発もできるだろうという安易な感覚でいました。しかし、実際はPythonを使うところは出てこず、node.js(typescript)でweb側の開発をすることになってだいぶ苦労しました。 それでも、先輩のエンジニアに0から教えてもらいながら研修を乗り越えることができました。  

とのこと。

くらまのシステムについても理解が深まったそうで、みんなの会話が徐々にわかるようになってきたみたいです。仕事をする上ではまずこれが重要ですね。

大変なことも多かったようですが、「何かを作ることが好き」+「みんなのマーケットで働く人の役に立ちたい」というモチベーションで取り組んでいたようです。

テクノロジー本部だけじゃなくて他部署の研修も参加できるよ!

そもそも、

  • くらしのマーケットってどういうサービスなのか?
  • どんな人がどんな風にサービスを使っているのか?


が理解できていないと開発する上で判断に困ることが出てきます。 なので、サービスについて理解するために、様々なプログラムや研修を用意しています。希望があれば他部署の研修でも参加することができます。

例えば、

  • ユーザーの気持ちを理解するために、予約申し込みから訪問サービスを受けるところまでを体験する
  • 事業者側の利用ルールを理解するために、くらしのマーケットへの出店登録から予約獲得までの流れを体験する
  • 出店している事業者向けの売上アップ講座に参加する

などがあります。売上アップ講座では、サービスを提供している出店者と直接話をすることもできるので、くらしのマーケットへの理解がより深まります 。

最後に

いかがでしたか?入社したら最初にどんなことをやるのかのイメージを持っていただけたでしょうか。
研修内容については受けた社員からフィードバックをもらい、次回の研修に役立てています。研修を受けた後にスムーズに開発に入れるような内容にしていきたいと思っています!

私たちテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています! 特に2019年新卒採用を積極的にしています。 こちらのサイトよりエントリーしていただいた方には個別説明会やカジュアル面談などを行なっております。
ぜひ応募お待ちしています!

Google Optimiseレポート!どんなデザインの変更ができるのか

はじめに

みんなのマーケットでUI/UXデザイナーをしているミソサクです。

Googleが提供するABテストツール「Google Optimise」を先日初めて使ったので、デザイン的にどんな設定が出来るのかまとめます。「Google Optimiseとは」とか「テストの設定方法」ではなく、見た目をどう変更できるのかに焦点を絞ってます。

テスト設定諸々を飛ばして、いきなりデザインの設定をする「ビジュアルエディタ」に来ます。こんな感じです。

f:id:curama-tech:20180719163539p:plain

「ビジュアルエディタ」で選択できる端末はこちら

  • 標準
  • Nexus7
  • iPad
  • Galaxy S5
  • Nexus5X
  • iPhone5
  • iPhone6
  • iPhone6 Plus
  • レスポンシブ(ベータ版)

※表示を切り替えてもいつまでも表示されない時があるので、再読み込みをするとすぐ切り替わります。

では、早速デザイン的に出来ることをご紹介します。

文字を変える

1. 変更したい要素を選択して「要素を編集」

f:id:curama-tech:20180719163548p:plain

2. 「テキストを編集」か「HTMLを編集」

f:id:curama-tech:20180719163559p:plain

3. 「テキストを編集」の場合、直接テキストを編集し、「完了」

f:id:curama-tech:20180719163852p:plain

3. 「HTMLを編集」の場合、html形式でテキストを編集し、「適用」

f:id:curama-tech:20180719163913p:plain

※ 編集画面を開いて変更する度に保存が必要です、それとは別にページ単位でも保存が必要です。

※ ビジュアルエディタでは変更前との「差分を取る」のではなく、「保存した回数分の変更」が保存されます。

要素を追加する

1. 要素を追加したい箇所の前の(もしくは後の)要素を選択して「要素を編集」

f:id:curama-tech:20180719163548p:plain

2. 「HTMLを挿入」

f:id:curama-tech:20180719163938p:plain

3. HTMLを編集

選択したタグがデフォルトで表示されます

f:id:curama-tech:20180719163952p:plain

4. オプションを選択して「適用」

選択できるオプション

  • 置換・・・そのまんまの意味です
  • 挿入・・・選択した要素内先頭に挿入されます
  • 要素内末尾に追記・・・そのまんまの意味です
  • 次より前・・・とんちみたいだけど単純に選択した要素の前に追加されます
  • 後に挿入・・・そのまんまの意味です

f:id:curama-tech:20180719164005p:plain

要素を削除する

1. 削除したい要素を選択して「要素を編集」

f:id:curama-tech:20180719163548p:plain

2. 「HTMLを編集」

f:id:curama-tech:20180719164028p:plain

3. コードごと削除して「適用」

デフォルトで選択されている「置き換え」以外のオプションを選択すると空のHTML挿入が変更として保存され、要素の削除にはならないのでご注意ください。

f:id:curama-tech:20180719164046p:plain

スタイルを変更する(ページ内の同じ要素全てを変更したい場合)

1. 「< >」アイコンをクリック

f:id:curama-tech:20180719164101p:plain

2 cssを記述して保存

f:id:curama-tech:20180719164119p:plain

スタイルを変更する(ページ内の特定の要素を変更したい場合)

1. 要素を選択してパレットからスタイルを指定して保存

f:id:curama-tech:20180719164133p:plain

パレット内で指定できるスタイル

f:id:curama-tech:20180719164146p:plain

要素の順番を変える

1. 要素をドラッグ&ドロップして保存

なんて簡単。

f:id:curama-tech:20180719164159p:plain

JavaScriptを実行

今回のテストではJavaScriptの変更はなかったので、次回テストする機会があればまたご紹介させていただきます。

さいごに

いかがでしたでしょうか?html、cssの知識がなくてもだいたいのことは出来そうですね!

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/

ExpressJSフレームワークの一つの単体テストの書き方

はじめに

こんにちは、みんなのマーケットのテックチームのクイです。

前回、ExpressJSフレームワークの紹介という記事で弊社のExpressJSフレームワークを利用している仕方を簡単に紹介しました。今回の続きはExpressJSフレームワークの一つの単体テスト(ユニットテスト)の書き方について紹介します。

単体テスト(ユニットテスト)とは

単体テストはプログラムを構成する部品単位(手続き型プログラミングの関数、クラスのメソッドなど)の動きが正しいかどうか検証するというテストです。

Javascriptにはmochajasmineなどの様々なテスティングフレームワークがあります。今回、mochachaisinonでユニットテストを実装します。

インストール

TypescriptとExpressJSのインストール仕方の参考はこちらです。

npm i --save-dev mocha chai sinon ts-node @types/mocha @types/chai @types/sinon

@types/sinonのバージョンによって使い方が違います。私は@types/sinon@4.3.3を使っています。

環境準備

まずはプロジェクトフォルダにtestフォルダを作成してtestフォルダの中unittestフォルダを作成します。 そして、package.jsonファイルに以下のような設定を追加します。

"scripts": {
    "test": "mocha -r ts-node/register src/**/test.ts",
},

これで、ts-nodeを用いてTypeScriptファイルを直接テストすることができます。

簡単に実装してみましょう

まず、テストしたい関数を作成します。

express/app/logic/example.ts

export function example(): string {
    return "テスト";
}

テストするファイルを宣言します。

express/test/unittest/test_example.ts

import { expect } from "chai";

import { example } from "../../app/logics/example";

describe("Example Test", () => {
    it("Example should return テスト", async () => {
        expect(example()).to.equal("テスト");
    });
});

テストを実行した結果は以下のような出力になります。

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Example Test
    ✓ Example should return テスト


  1 passing (24ms)

上は簡単な例ですが、Dependency Injectionのような複雑なクラスの場合はどうすれば良いでしょう。

SinonでDependency Injection使うクラスをテスト仕方

仮に二つのクラスがあるとします。 VehicleというクラスのstartVehicle()メソッドをテストしたいです。 EngineVehicleに注入されるクラスです。

express/app/logics/engine.ts

export enum EngineStatus {
    Idle = "idle",
    Running = "running"
}

export class Engine {
    private status: EngineStatus = EngineStatus.Idle;

    public getEngineStatus(): EngineStatus {
        return this.status;
    }
}

express/app/logics/vehicle.ts

import { Engine, EngineStatus } from "./engine";

export class Vehicle {
    constructor(
        private engine: Engine
    ) { }

    public getVehicle() {
        if (this.engine.getEngineStatus() === EngineStatus.Idle) {
            return "Vehicle is ready";
        } else {
            return "Vehicle is busy";
        }
    }
}

テストするファイル:

express/test/unittest/test_vehicle.ts

import { expect } from "chai";
import * as sinon from "sinon";

import { Engine, EngineStatus } from "../../app/logics/engine";
import { Vehicle } from "../../app/logics/vehicle";

describe("Test Vehicle", () => {
    let sandbox: sinon.SinonSandbox;
    let vehicle: Vehicle;

    beforeEach(() => {
        sandbox = sinon.createSandbox();
        const engine = sandbox.createStubInstance(Engine);
        engine.setEngineStatus = (status: EngineStatus): EngineStatus => { // setEngineStatusメソッドをモックする。
            return EngineStatus.Running;
        };

        vehicle = new Vehicle(engine);
    });

    afterEach(() => {
        sandbox.restore();
    });

    it("Vehicle should be busy", () => {
        expect(vehicle.getVehicle()).to.equal("Vehicle is busy");
    });
});

テストの結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Test Vehicle
    ✓ Vehicle should be busy


  1 passing (18ms)

テストが失敗するケースを実装してみます。 テストファイルを以下のように修正してください。

it("Vehicle should be busy", async () => {
    expect(vehicle.getVehicle()).to.equal("Vehicle is ready");
});

結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Test Vehicle
    1) Vehicle should be busy


  0 passing (45ms)
  1 failing

  1) Test Vehicle
       Vehicle should be busy:

      AssertionError: expected 'Vehicle is busy' to equal 'Vehicle is ready'
      + expected - actual

      -Vehicle is busy
      +Vehicle is ready
      
      at Object.<anonymous> (test/unittest/test_vehicle.ts:26:41)
      at next (native)
      at /Users/quynv/Documents/express/test/unittest/test_vehicle.ts:7:71
      at __awaiter (test/unittest/test_vehicle.ts:3:12)
      at Context.it (test/unittest/test_vehicle.ts:25:34)



npm ERR! Test failed.  See above for more details.

カバレッジを導入する

まず、必要なパッケージをインストールします。

npm i --save-dev nyc source-map-support

次にpackage.jsonファイルを以下のように修正します。

  "scripts": {
    "test": "nyc mocha"
  },
  ....
  "nyc": {
    "check-coverage": true,
    "extension": [
      ".ts"
    ],
    "require": [
      "ts-node/register",
      "source-map-support/register"
    ],
    "reporter": [
      "lcov",        //どこのまだ実行されないコードが見える
      "text-summary" //結果テキストのもとに見える
    ],
    "exclude": [     //除きたいフォルダ
      "controllers",
      "vendor",
      "bin",
      "test"
    ],
    "report-dir": "./test/coverage/", //テストの結果のHTMLファイルが保存する場所。
    "sourceMap": true,
    "instrument": true
  }

testフォルダの中mocha.optsファイルを作成して以下のような内容を入力します。

/test/mocha.opts

--r ts-node/register
--r source-map-support/register
--full-trace
--bail
test/**/*.ts

テストの結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> nyc mocha



  Test Vehicle
    ✓ Vehicle should be busy


  1 passing (18ms)

ERROR: Coverage for lines (75%) does not meet global threshold (90%)

=============================== Coverage summary ===============================
Statements   : 75% ( 9/12 )
Branches     : 75% ( 3/4 )
Functions    : 60% ( 3/5 )
Lines        : 75% ( 9/12 )
================================================================================
npm ERR! Test failed.  See above for more details.

全部のテストが成功しましたが、カバレッジの各値が低いように見えます。

test/coverage/lcov-report/index.htmlファイルを見ましょう f:id:curama-tech:20180711181520p:plain vehicle.tsをクリックして f:id:curama-tech:20180711181540p:plain

上の結果を見るとvehicleファイルに実行していないコードがまだあることがわかります。

つまり、テストケースがまだ足りませんでした。修正しましょう。

engine.tsファイルをテストしたくない場合はファイルの上に/* istanbul ignore file */を挿入して、テストするファイルを以下のように修正します。

describe("Test Vehicle", () => {
    let sandbox: sinon.SinonSandbox;
    let vehicle: Vehicle;
    let engine: Engine;

    beforeEach(() => {
        sandbox = sinon.createSandbox();
        engine = sandbox.createStubInstance(Engine);
        engine.getEngineStatus = (): EngineStatus => {
            return EngineStatus.Running;
        };

        vehicle = new Vehicle(engine);
    });

    afterEach(() => {
        sandbox.restore();
    });

    it("Vehicle should be busy", async () => {
        expect(vehicle.getVehicle()).to.equal("Vehicle is busy");
    });

    it("Vehicle should be ready", async () => {
        engine.getEngineStatus = (): EngineStatus => {
            return EngineStatus.Idle;
        };

        vehicle = new Vehicle(engine);
        expect(vehicle.getVehicle()).to.equal("Vehicle is ready");
    });
});

修正したテストの結果

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> nyc mocha



  Test Vehicle
    ✓ Vehicle should be busy
    ✓ Vehicle should be ready


  2 passing (15ms)

=============================== Coverage summary ===============================
Statements   : 100% ( 6/6 )
Branches     : 100% ( 2/2 )
Functions    : 100% ( 2/2 )
Lines        : 100% ( 6/6 )
================================================================================

f:id:curama-tech:20180711181529p:plain f:id:curama-tech:20180711181545p:plain

以上です。

最後に

みんなのマーケット達と一緒に働く仲間を募集しています!。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/ )。

エンジニア紹介 Vol.1

f:id:curama-tech:20180706112744j:plain

こんにちは。ディレクターの塚本です。

今回は同じチームのエンジニアを紹介します!チームには5名のエンジニアがいて、日々アプリやWebの設計・開発を行っています。それではさっそくまいりましょう。

時々関西弁を話すネパール人、カーキ・スシャーン

  • <エンジニア歴>4年
  • <得意な言語>Typescript、 場合によってPython
  • <興味のある技術と理由>Full stack data science。Data scienceに興味があるけど、単純に誰からデータもらって分析だけじゃなくて、こんなデータがあれば面白いかもって思ったら自分でData engineeringできたら楽しそうだから。
  • <みんマの好きなところ>自由な感じが好き
  • <みんマに入社した理由>会社が好き + 日本で働いてみたかった

f:id:curama-tech:20180706112802j:plain

カーキさんは日本で育ったんじゃないかと思うほど流暢な日本語を話しますが、漢字は大嫌いで苦手です(今はわかりませんが数年前は弁当屋のメニューにある漢字が読めず、毎回唐揚げ弁当を注文していました)。また、誰に対しても優しいうえ、端正な顔立ちで会社の人気者です。

猫をこよなく愛する開発部門の父、のりすけ

  • <エンジニア歴>どこから数えればいいかわからなかったけど8年くらい
  • <好きな言語>Haskell, Typescript, Rust
  • <興味のある技術と理由>Haskell。値がなにかではなくこの型はなにが出来るかを考え、強力な型システムでやりたいことを抽象化し設計できる。
  • <みんマの好きなところ>猫がいる
  • <みんマに入社した理由>猫がいる
  • <みんマの最初の印象と実際>最初: 猫がいる、実際: 猫がいる

f:id:curama-tech:20180706112806j:plain

のりすけさんは開発に関する知識や経験が豊富で他のエンジニアからとても頼りにされています。仕事に関しては非常に知的で尊敬していますが、仕事以外の話になると途端にアホになることがあります。そのギャップがたまりません。

天才かよ、ヤン・ヤン

  • <エンジニア歴>5年くらい(大学院の時教授の会社でフルタイム2年半+日本で1年SE+みんマで2年未満)
  • <好きな言語>Kotlin
  • <興味のある技術と理由>最近はFlutterに気になります。エンジニアに対して、 Write once, run everywhere はいつも吸引力がありますから。
  • <エンジニアになった理由>金融系やSE管理の仕事もそれぞれ一年間試したことがありますが、やはりもの作りが一番楽しいと思います。
  • <みんマの好きなところ>エンジニアとして好きなところは三つがあって、自由と自由そして自由です。

f:id:curama-tech:20180706112818j:plain

ヤンさんは中国出身です。「次これをお願いしたいんですけど〜」と言うと、「もう終わってます」という会話が何度なされたことか。仕事が早い、かつ丁寧。天才かよ。

とにかく前へ!Duy 

  • <エンジニア歴>一年間ぐらいWebのエンジニアとして、プログラミングしています。一年半ほどSwiftでiOSのアプリを開発しています。
  • <好きな言語>pythonとSwiftです。
  • <興味のある技術と理由>データ分析と機械学習。データを分析して、機械をトレーニングして、ユーザーの行動を推測するのは引力があります。そして、実際にこの技術だとこれからたくさん分野に応用できると思います。
  • <エンジニアになった理由>技術のプロダクトとソフトウェアに興味があって、自分で技術のプロダクトを作りたいと思います。
  • <こんな人と働きたい>創造性がある人と技術のスキールを持っている人。

f:id:curama-tech:20180706112754j:plain

Duyくんはベトナム出身です。持ち前のコミュ力を活かして日本語も開発技術もぐんぐん吸収していきます。これまでDuyくんといっしょに仕事をする機会が多かったのですが、分からなくてもミスってもとにかく前へ!前へ!という気持ちをとても感じます。

困ったら笑う!Tuyen

  • <エンジニア歴>Webを開発(1年半)+みんま(1年半)
  • <好きな言語>Python, Typescript
  • <興味のある技術と理由>最近、設計システムに興味があります。開発するとき、いつもいい設計を考える必要ですから、もっと勉強したいです。
  • <エンジニアになった理由>ソフトウェア、ウェブサイトを作れたら、とても嬉しく思ったからです。
  • <みんマの好きなところ>フレックスタイム

f:id:curama-tech:20180706112812j:plain

TuyenくんもDuyくんと同じベトナム出身です。日本語でのコミニュケーションがうまくいかない時は大体笑ってごまかします。その笑顔は人を和ませる力を持っているので、かなりキャラ得です。Tuyenくんはできる素振りは一切見せずに実はアプリの開発ができたり数学が得意だったりと、まだ明かされてない特技を秘めているような気がします。

さいごに

いかがでしたでしょうか?
もっとエンジニアの話を聞いてみたいと思った方はぜひお気軽にご連絡ください!(コーポレートサイト https://www.minma.jp/

Implementing a simple pipeline with Typescript

Implementing a simple pipeline with Typescript

Hi, this is Sushant from the Minma, Inc. tech-team and today I would like to demonstrate to you the power of Typescript. In my last post, I introduced you to Apache Airflow and demonstrated the implementation of a pipeline to create a pizza using Airflow. Today, we will do the same, but with Typescript. The goal is to build a strongly typed pipeline utilizing Typescript's type system and create a variety of pizzas with it.

As you can probably tell, I am somewhat obsessed to pipelines. To me, pipelines are a great way to write code for some business process. Add static typing to pipelines and you get a pipeline that is robust, modular and flexible and readable. Let's say I work for an e-commerce site (which I actually do ;)) and we just got reports that our users never get any welcome email upon registration. Not knowing if it's a bug in the system or if we ever send emails in the first place, my product manager asks me if we are sending welcome emails when user registration is complete. Being a relatively new programmer in the company, I have no other way to answer that than go through the source code for and find out what happens during user registration. A lot of time this would involve going through a lot of code, jumping around from function to function trying to get to code that relates to the end of the registration process. If the same user registration process were implemented as a pipeline, I would be able to get a top level view of what goes on when a user registers at our site just by looking at the pipeline's steps. Chances are, there could actually be a step in the pipeline that reads "sendWelcomeEmail"! Even if that's not the case, I could just look at the pipeline and choose a step in the pipeline that I think could have a connection to emails and see what's going on.

Enough talk! Let's write ourselves a pipeline with Typescript.

Here's what we are gonna do: - a. Implement a function that lets us create pipeline for any workflow. - b. Use the function to create a pipeline for creating pizzas based on a user's order. - c. Use the pipeline created in step b. to create various kinds of pizzas.

The source code for the pipeline is available in github

a. Implement a function that lets us create pipeline for any workflow.

We want our module to take in functions that represent each step in our pipeline and return a single composed function that performs those steps in order.

If f(x), g(x), h(x) and i(x) represent each step in our pipeline, the definition of our workflow would be

            j(x) = i ( h ( g ( f ( x ) ) ) ) 

So, whenever we want to run our workflow, we don't directly call f(x), g(x), h(x), and i(x) in order every single time. All we need to do is call j(x) and the pipeline runs as expected.

Here is the definition in typescript:

export type Pipeable<Args, ReturnType> = (args: Args) => ReturnType;

export function createPipeline<X, A>(f: Pipeable<X, A>): Pipeable<X, A>;
export function createPipeline<X, A, B>(f: Pipeable<X, A>, g: Pipeable<A, B>): Pipeable<X, B>;
export function createPipeline<X, A, B, C>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>): Pipeable<X, C>;
export function createPipeline<X, A, B, C, D>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>, i: Pipeable<C, D>): Pipeable<X, D>;
export function createPipeline<X, A, B, C, D, E>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>, i: Pipeable<C, D>, j: Pipeable<D, E>): Pipeable<X, E>;
export function createPipeline<X, A, B, C, D, E, F>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>, i: Pipeable<C, D>, j: Pipeable<D, E>, k: Pipeable<E, F>): Pipeable<X, F>;
export function createPipeline<X, A, B, C, D, E, F>( 
    f: Pipeable<X, A>, 
    g?: Pipeable<A, B>, 
    h?: Pipeable<B, C>, 
    i?: Pipeable<C, D>, 
    j?: Pipeable<D, E>, 
    k?: Pipeable<E, F>): 
    Pipeable<X, A> | 
    Pipeable<X, B> |
    Pipeable<X, C> | 
    Pipeable<X, D> | 
    Pipeable<X, E> | 
    Pipeable<X, F> {
        if (!!k) {
            return (args) => k(j!(i!(h!(g!(f(args))))));
        } else if (!!j) {
            return (args) => j(i!(h!(g!(f(args)))));
        } else if (!!i) {
            return (args) => i(h!(g!(f(args))));
        } else if (!!h) {
           return (args) => h(g!(f(args)));
        } else if (!!g) {
            return (args) => g(f(args));
        } else {
            return (args) => f(args);
        }
}

Let's break this down one by one.

export type Pipeable<Args, ReturnType> = (args: Args) => ReturnType;

Pipable<Args, ReturnType> is a generic type for any function that takes in arguments of type Args and returns something of type ReturnType. We will be using this to generate your compose function.

Our main function createPipeline() has six overloads, one for each number of arguments passed to it. This implies that our create pipeline function can handle workflows having no more than 6 steps.
Let's look at one of these overloads:

export function createPipeline<X, A, B, C>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>): Pipeable<X, C>;

If we were to visualize what createPipeline does, it would look something like this:

It takes in 3 functions f, g, and h and returns a new composed function that does the same thing as the three functions combined. Notice that the input of the composed function is the same as the input of the first step of the pipeline and the output is the same as the output of the last step of the pipeline. What this means is, instead of running f, g, and h in order everytime, we can just run this composed function every time we want to execute our pipeline and be assured that the result will be the same as running f, g and h combined.

if (!!k) {
    return (args) => k(j!(i!(h!(g!(f(args))))));
} else if (!!j) {
    return (args) => j(i!(h!(g!(f(args)))));
} else if (!!i) {
    return (args) => i(h!(g!(f(args))));
} else if (!!h) {
   return (args) => h(g!(f(args)));
} else if (!!g) {
    return (args) => g(f(args));
} else {
    return (args) => f(args);
}

↑This is not code that I am proud of, but in order to ensure that the types are inferred correctly, I had to write it this way.

What I really wanted to do was to implement a reducer like so

 const reducer: <M, N, O>(f: Pipeable<M, N>, g: Pipeable<N, O>) => Pipeable<M, O> = (f, g) => (x) => g(f(x));

and use the reducer function to reduce an array of functions like

[f, g, h, i, j, k].reduce(reducer)

, but sadly Typescript wasn't so forigiving. If anybody has a more elegant solution, please enlighten me!

b. Use the function to create a pipeline for creating pizzas based on a user's order.

This is where we build the building blocks of our pipeline. Let's first define what an order looks like:

export namespace OrderOptions {
    export enum Crust {
        HandTossed = "HandTossed",
        Pan = "Pan",
        ThinCrust = "ThinCrust"
    }

    export enum Sauce {
        Tomato = "Tomato"
    }

    export enum Topping {
        Ham = "Ham",
        Cheese = "Cheese",
        Pineapple = "Pineapple",
        Mushroom = "Mushroom",
        Pepperoni = "Pepperoni",
        ItalianSausage = "Italian Sausage",
        Olives = "Olives",
        Jalapeno = "Jalapeno",
        GreenPepper = "Green Pepper",
        Anchovies = "Anchovies",
        Onion = "Onion"
    }
}

export interface Order {
    crust: OrderOptions.Crust;
    sauce: OrderOptions.Sauce;
    toppings: OrderOptions.Topping[];
}

Customers can select from 3 types of crusts, sadly just one type of sauce and many different kinds of toppings. Our order object, as is obvious from the model, will have a value for crust, sauce and an array of toppings;

Our pipeline for creating a pizza will have the following 4 steps:

  1. Prepare crust.
  2. Apply sauce.
  3. Add toppings.
  4. Bake in oven.

Note that each step is dependent on the previous step. i.e. We cannot apply sauce if you have no crust. We don't add toppings if the sauce hasn't been applied yet, and so on. Therefore, we define state for our pizza as follows:

type CrustPrepared = { crust: OrderOptions.Crust };
type SauceApplied = CrustPrepared & { sauce: OrderOptions.Sauce };
type ToppingsAdded = SauceApplied & { toppings: OrderOptions.Topping[] }
type FreshlyBakedPizza = ToppingsAdded & { baked: true };

type PizzaState = CrustPrepared | SauceApplied | ToppingsAdded | FreshlyBakedPizza;

As we can see, if our pizza is in the SauceAppliedstate, it will have properties from the CrustPrepared state and have one more property for the type of sauce applied. Note that if our pizza is in the ToppingsAddedstate, it will have properties from both the CrustPrepared state the SauceAdded state in addition to the property for the types of toppings used.

To make life easier, we define following type to represent the output at every step in the pipeline:

interface Output<CurrentState extends PizzaState> {
    readonly order: Order,
    pizzaState: CurrentState,
    events: string[];
}

The order property represents the order made by the customer. We don't want the order to suddenly change when the pizza is half done, so we set it as readonly. The pizzaState property represents the current state in the pipeline. We also have an events property for logging purposes so that we can see that all the steps in the pipeline were properly executed and in order.

OK, let's start writing code for each step of the pipeline:

1. Prepare crust.

Our prepareCrust step looks like this:

const prepareCrust: Pipeable<Order, Output<CrustPrepared>> = (order) => {
    return { 
        order: order,
        pizzaState: { crust: order.crust },
        events: [`Preparing ${order.crust} pizza crust......DONE`]
    }
}

Remember that each function passed to our createPipeline function has type Pipeable. Thus, we define our prepareCrust function as Pipeable. We are not doing anything crazy. All we are doing is take the order, and set the pizzaState with crust set to the crust ordered by the user. We also add a message to log that this step completed successfully.

2. Apply sauce.

Similarly, our applySauce step looks like this:

const applySauce: Pipeable<Output<CrustPrepared>, Output<SauceApplied>> = (state) => {
    return { 
        ...state,
        pizzaState: { ...state.pizzaState, sauce: state.order.sauce },
        events: [...state.events, `Applying ${state.order.sauce} sauce......DONE`]
    }
}

Things to note:

  • This function takes the output of the prepareCrust step as an argument.
  • We are using the spread operator ...state to first create a copy of the current state, and then update the state in the copy with the new state.

3. Add toppings.

Our addToppings step looks like this:

const applyToppings: Pipeable<Output<SauceApplied>, Output<ToppingsAdded>> = (state) => {
    return { 
        ...state,
        pizzaState: { ...state.pizzaState, toppings: state.order.toppings },
        events: [...state.events, `Adding ${state.order.toppings.join(", ")} toppings......DONE`]
    }
}

Nothing special here, similar to the applySauce.

4. Bake in oven.

Finally, our bakeInOven step looks something like this:

const bakeInOven: Pipeable<Output<ToppingsAdded>, Output<FreshlyBakedPizza>> = (state) => {
    return { 
        ...state,
        pizzaState: { ...state.pizzaState, baked: true },
        events: [...state.events, "Baking in oven......DONE", "Freshly baked pizza ready!!!!"]
    }
}

Now that we have functions for each of our steps, let's create our pipeline:

export const createPizza = createPipeline(
    prepareCrust,
    applySauce,
    applyToppings,
    bakeInOven
);

We finally get to use our createPipeline function to create our pizza pipeline. Note that if we tried to change the order of the steps here, Typescript would not let us! This is where all the hard work of writing the overloads for createPipeline pays off. This is the part that also shows the readability of pipelines. I could just look at this piece of code and have an idea about what steps are involved in creating a pizza. Of course, good naming goes hand in hand for that to work.

Notice that we are not exporting the individual steps. We are just exporting the composed createPizza function. So, if somebody wants to create a pizza, they have no choice but to adhere to our createPizza pipeline. We can extend this concept and do things differently. Instead of having a generic createPizza for all types of pizzas, we could have a pipeline for each type of pizza we offer such as createHawaiianPizza or createMeatLoversPizza and so on. The possibilities are endless!

Now that we have our pipeline ready, let's order some pizzas:

const handTossedHawaiianPizza = createPizza({
    crust: OrderOptions.Crust.HandTossed,
    sauce: OrderOptions.Sauce.Tomato,
    toppings: [OrderOptions.Topping.Ham, OrderOptions.Topping.Pineapple, OrderOptions.Topping.Cheese]
})

const thinCrustHawaiianPizza = createPizza({
    crust: OrderOptions.Crust.ThinCrust,
    sauce: OrderOptions.Sauce.Tomato,
    toppings: [OrderOptions.Topping.Ham, OrderOptions.Topping.Pineapple, OrderOptions.Topping.Cheese]
})

const meatLoversPizza = createPizza({
    crust: OrderOptions.Crust.Pan,
    sauce: OrderOptions.Sauce.Tomato,
    toppings: [OrderOptions.Topping.ItalianSausage, OrderOptions.Topping.Ham, OrderOptions.Topping.Pepperoni ,OrderOptions.Topping.Cheese ]
})


console.log("######### Hand Tossed Hawaiian pizza ##########")
console.log(handTossedHawaiianPizza)
console.log("###############################################\n")

console.log("######### Thin CrustHawaiian pizza ############")
console.log(thinCrustHawaiianPizza)
console.log("###############################################\n")

console.log("############# Meat Lovers pizza ###########")
console.log(meatLoversPizza)
console.log("###############################################\n")

All we are doing is passing an Order object to our createPizza pipeline and logging the results to the console.

To see if it works, compile with tsc and run.

If using the source code from github (assuming that Typescript is installed), you can test it like so:

# Inside the src directory
$ tsc && node bin/orderPizza.js

Output:

######### Hand Tossed Hawaiian pizza ##########
{ order:
   { crust: 'HandTossed',
     sauce: 'Tomato',
     toppings: [ 'Ham', 'Pineapple', 'Cheese' ] },
  pizzaState:
   { crust: 'HandTossed',
     sauce: 'Tomato',
     toppings: [ 'Ham', 'Pineapple', 'Cheese' ],
     baked: true },
  events:
   [ 'Preparing HandTossed pizza crust......DONE',
     'Applying Tomato sauce......DONE',
     'Adding Ham, Pineapple, Cheese toppings......DONE',
     'Baking in oven......DONE',
     'Freshly baked pizza ready!!!!' ] }
###############################################

######### Thin CrustHawaiian pizza ############
{ order:
   { crust: 'ThinCrust',
     sauce: 'Tomato',
     toppings: [ 'Ham', 'Pineapple', 'Cheese' ] },
  pizzaState:
   { crust: 'ThinCrust',
     sauce: 'Tomato',
     toppings: [ 'Ham', 'Pineapple', 'Cheese' ],
     baked: true },
  events:
   [ 'Preparing ThinCrust pizza crust......DONE',
     'Applying Tomato sauce......DONE',
     'Adding Ham, Pineapple, Cheese toppings......DONE',
     'Baking in oven......DONE',
     'Freshly baked pizza ready!!!!' ] }
###############################################

############# Meat Lovers pizza ###########
{ order:
   { crust: 'Pan',
     sauce: 'Tomato',
     toppings: [ 'Italian Sausage', 'Ham', 'Pepperoni', 'Cheese' ] },
  pizzaState:
   { crust: 'Pan',
     sauce: 'Tomato',
     toppings: [ 'Italian Sausage', 'Ham', 'Pepperoni', 'Cheese' ],
     baked: true },
  events:
   [ 'Preparing Pan pizza crust......DONE',
     'Applying Tomato sauce......DONE',
     'Adding Italian Sausage, Ham, Pepperoni, Cheese toppings......DONE',
     'Baking in oven......DONE',
     'Freshly baked pizza ready!!!!' ] }
###############################################

And that's it for this post! I hope I could demonstrate the usefulness of pipelines, especially with Typescript. I'm sure there are better ways to implement the same (and I would be all ears for better ways). Hopefully in a future post, I will be able to demonstrate a better way to implement the same pipeline, perhaps with some error handling and much cleaner code!

PS: We are hiring! For those interested, feel free to apply here . At least conversational Japanese proficiency is required.