料理のレシピをJSONで表現してみる試み 

# はじめに
ある日、ふと「料理のレシピを機械的に表現したい」という思いに駆られました。料理において、例えば「じゃがいもを切る」という工程は、入力として「じゃがいも」を渡すと、出力として「切られたじゃがいも」が出てくる関数のようなものなわけで、その関数群を一定のルールで表記できれば、レシピ全体を表現できそうです。
考えたことを記録として残してみようと思います。

## 表記ルール
取り扱いやすさを考え、JSONで表現することにしました。AWSのCloudFormationやStep Functionsなどを参考に組み立てています。
### 概要
レシピは、大きく分けて以下の3つの要素から構成される、と考え、各要素を列挙することにより全体を表現します。

|要素|説明|
|:--|:--|
|素材(material)|「玉ねぎ」「砂糖」「牛肉」など、料理の材料となるもの|
|容器(container)|「鍋」「どんぶり」「ボウル」など、素材を入れるもの|
|操作(action)|「切る」「煮る」など、素材の状態を変えるもの|

全体像は以下のような感じです。

```txt:全体像
{
  "title": "名前",
  "description": "説明",
  "materials": { /* 素材を列挙 */ },
  "containers": { /* 容器を列挙 */ },
  "actions": { /* 操作を列挙 */ }
}
```

### 素材(material)
素材は、名前の通り料理の材料となるものです。記載方法の例は以下です。

```txt:素材(玉ねぎ)
{
  …
  "materials": {
    "onion1": {
      "title": "玉ねぎ",
      "type": "onion",
      "description": "新鮮な玉ねぎ"
      "quantity": [
        {
          "amount": 0.5,
          "unit": "pieces"
        },
        {
          "amount": 100,
          "unit": "g"
        }
      ]
    },
    …
  }
}
```
|要素||説明|
|:--|:--|:--|
|title||素材の表示名。省略した場合はtypeにより自動で決まる。typeとしてcustomを指定した場合は必須。|
|type||素材の種別。onion(玉ねぎ)、sugar(砂糖)などを事前に定義しておく。任意の素材としてcustomを指定することも可。|
|description||素材の説明。省略可。|
|quantity||分量。「玉ねぎ半個(100g)」のように、1つの素材に複数の分量がある場合があるので配列にする。|
||amount|素材の量|
||unit|素材の量の単位。g(グラム)、pieces(個)、tbsp(大さじ)など|
後述しますが、今回は「牛丼」を題材として考えたため、typeやunitは牛丼に必要なものしか定義していないです。レシピを増やしながら定義を拡充していきたいところです。

### 容器(container)
容器は、複数の素材をグループ化するときに利用します。例えば「煮る」という操作は複数の素材に対して同時に行われます。そのような場合は、容器を使って素材をひとまとめにし、その容器に対して「煮る」操作を適用する、と考えます。容器の記載例は以下です。

```txt:容器(鍋)
{
  …
  "containers": {
    "pot1": {
      "title": "お鍋",
      "type": "pot",
      "description": "大き目の鍋。蓋も必要。"
    },
    …
  }
}
```
|要素|説明|
|:--|:--|
|title|容器の表示名。省略した場合はtypeにより自動で決まる。typeとしてcustomを指定した場合は必須。|
|type|容器の種別。pot(鍋)、riceBowl(どんぶり)などを事前に定義しておく。任意の容器としてcustomを指定することも可。|
|description|容器の説明。省略可。|
実際に容器を使って素材をグループ化するには、後述する「add」アクションを使います。


### 操作(action)
操作は、素材や容器に対して適用され、何等かの形で状態を変化させるものです。例は以下です。

```txt:操作(玉ねぎを切る->玉ねぎを鍋に入れる)
{
  …
  "actions": {
    "cutOnion": {
      "type": "cut",
      "source": "onion1",
      "description": "繊維と平行に、幅1センチ程度になるようにスライスする。"
    },
    "addOnionToPot": {
      "type": "add",
      "source": "onion1",
      "target": "pot1",
      "description": "玉ねぎを鍋に投入する。",
      "depend": "cutOnion"
    },
  …
  }
}
```
|要素||説明|
|:--|:--|:--|
|title||操作の表示名。省略した場合はtypeにより自動で決まる。typeとしてcustomを指定した場合は必須。|
|type||操作の種別。cut(切る)、add(加える)、stew(煮る)などを事前に定義しておく。任意の操作としてcustomを指定することも可。|
|source||操作の入力元となる素材、又は容器。typeによっては配列で複数指定も可。|
|target||操作の出力先となる素材、又は容器。|
|description||操作の説明。省略可。|
|depend||依存関係。その操作を行う前に終わらせておくべき操作を指定する。配列で複数指定も可。|
|until||アクションに終了条件(…分経過するまで、柔らかくなるまで、など)がある場合は指定。|
||type|終了条件のタイプ。今のところtime(…分経過するまで)のみ|
||value|untilの値|

上の例だと、玉ねぎを鍋に入れる前には玉ねぎを切っておく必要があるので、addOnionToPot(玉ねぎを鍋に入れる)のdepend属性としてcutOnion(玉ねぎを切る)を指定しています。実行順序ではなく依存関係で考えるのがポイントで、例えば「人参を切る」と「玉ねぎを切る」はどちらを先に実行してもいいので依存関係はありませんが、「玉ねぎの皮をむく」と「玉ねぎを切る」は、皮むきのほうが先のはずなので依存関係を持たせるべきです。

また、addOnionToPotは、容器を使って素材をグループ化している例です。addアクションを使って容器「pot1(鍋)」に「onion1(玉ねぎ)」を追加しています。素材をグループ化するためには実際に手を動かす(鍋に入れる、ボウルに入れる、など)必要があるので、その都度アクションを割り当てる、という発想になります。

### 全体像
ここまでを踏まえて作った牛丼のレシピが以下となります。
<details><summary>クリックで展開</summary><div>

```json:牛丼.json
{
  "title": "牛丼",
  "description": "薄く切った牛肉とタマネギなどを醤油などで甘辛く煮込み、丼に盛った飯の上に載せた料理",
  "containers": {
    "pot1": {
      "type": "pot"
    },
    "riceBowl1": {
      "type": "riceBowl",
      "description": "一般的などんぶり"
    }
  },
  "materials": {
    "rice1": {
      "type": "rice",
      "quantity": [
        {
          "amount": 1,
          "unit": "go"
        }
      ]
    },
    "water1": {
      "type": "water",
      "quantity": [
        {
          "amount": 150,
          "unit": "ml"
        }
      ]
    },
    "sugar1": {
      "type": "sugar",
      "quantity": [
        {
          "amount": 1,
          "unit": "tbsp"
        }
      ]
    },
    "soySauce1": {
      "type": "soySauce",
      "description": "一般的なこいくち醤油",
      "quantity": [
        {
          "amount": 3,
          "unit": "tbsp"
        }
      ]
    },
    "mirin1": {
      "type": "mirin",
      "quantity": [
        {
          "amount": 3,
          "unit": "tbsp"
        }
      ]
    },
    "ginger_tube1": {
      "type": "ginger_tube",
      "quantity": [
        {
          "amount": 1,
          "unit": "cm"
        }
      ]
    },
    "beefRib": {
      "title": "牛バラ肉",
      "type": "beef",
      "description": "薄く細切りの牛バラ肉",
      "quantity": [
        {
          "amount": 200,
          "unit": "g"
        }
      ]
    },
    "onion1": {
      "type": "onion",
      "quantity": [
        {
          "amount": 0.5,
          "unit": "pieces"
        },
        {
          "amount": 100,
          "unit": "g"
        }
      ]
    }
  },
  "actions": {
    "cookRice": {
      "type": "cookRice",
      "source": "rice1",
      "description": "炊飯器で米を炊く。"
    },
    "makeBroth": {
      "title": "煮汁を作る",
      "type": "add",
      "source": [
        "water1",
        "sugar1",
        "soySauce1",
        "mirin1",
        "ginger_tube1"
      ],
      "target": "pot1",
      "description": "各調味料を計量し、鍋に入れる。"
    },
    "cutBeefRib": {
      "type": "cut",
      "source": "beefRib",
      "description": "長辺5センチ程度になるようにカットする。大きさを揃えておくと、味の染み込み具合や食感を均一にできる。"
    },
    "peelOnion": {
      "type": "peel",
      "source": "onion1",
      "description": "2分割して固いところを切り落とした後、素手で皮をむく。"
    },
    "cutOnion": {
      "type": "cut",
      "source": "onion1",
      "description": "繊維と平行に、幅1センチ程度になるようにスライスする。",
      "depend": "peelOnion"
    },
    "boil": {
      "type": "bringToABoil",
      "source": "pot1",
      "description": "調味料が入った鍋を強火で沸騰させる。",
      "depend": "makeBroth"
    },
    "addBeefRibToPot": {
      "type": "add",
      "source": "beefRib",
      "target": "pot1",
      "description": "中火に変更し、牛肉を加えて解きほぐす。",
      "depend": [
        "cutBeefRib",
        "boil"
      ]
    },
    "stew1": {
      "type": "stew",
      "source": "pot1",
      "until": {
        "type": "time",
        "value": 5
      },
      "description": "中火のまま5分間煮込む。灰汁が気になる場合は取り除いておく。",
      "depend": "addBeefRibToPot"
    },
    "addOnionToPot": {
      "type": "add",
      "source": "onion1",
      "target": "pot1",
      "description": "玉ねぎを鍋に投入する。",
      "depend": [
        "cutOnion",
        "stew1"
      ]
    },
    "serve": {
      "type": "serve",
      "source": [
        "rice1",
        "pot1"
      ],
      "target": "riceBowl1",
      "description": "どんぶりにご飯と具材を盛り付ける。",
      "depend": [
        "cookRice"
      ]
    }
  }
}
```

</div>

## 視覚化
JSONでの表現ができたところで、せっかくなので[ビューアを作って視覚化](https://cooking-workflow.herokuapp.com/makeRecipe)してみました。
![recipe.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114715/9bc5e54f-d505-5063-1136-43eb5493bec1.jpeg)
素材や容器で指定されたtypeに応じて、わかりやすいように画像を表示させています。依存関係をきちんと定義すれば、ワークフローのようにレシピを表現できるはずです。

# さいごに
まだ牛丼のことしか考えていないので、表現力が足りていないところがあるかもしれません。他にもレシピをJSON化しながら足りないところを足していきたいです。
また、レシピをこのように構造的に表現できれば、クリティカルパスの計算や、それに基づいた作業順序の推奨、進捗の管理などなど、できることの想像が広がります。色々試してみようと思います。

# 参考文献

レシピ部分で以下を参考にさせて頂きました。
・チューブ生姜適量ではなくて1cmがいい人の理系の料理