Pythonエンジニアのための「計算機プログラムの構造と解釈」学習環境

この記事は[セーフィー株式会社 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/safie) の18日日目の記事です！

[セーフィー株式会社](https://safie.co.jp/)でクラウドカメラ向けの画像認識の開発エンジニアをしています。

この記事では普段の業務から離れて、コンピュータサイエンスの分野では古典的名著と言われている[「計算機プログラムの構造と解釈」](https://amzn.asia/d/1xDx9W5)（以下SICP）の学習を行うためのScheme環境として、[Calysto Scheme](https://github.com/Calysto/calysto_scheme)を紹介したいと思います。

![sicp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/50856f63-5755-710e-18d7-5da8b3dcae40.png)


# Calysto Schemeとは
SICPを読むにあたってScheme環境を準備する必要があります。今回紹介するCalysto SchemeはScheme環境のひとつで以下の特徴があります。

- Jupyter Notebook上で動作し、対話的にコーディングを行うことができ、数式も含めて1つのNotebookとして保存できる
- Visual Studio Code上で開発することできる
- Pythonコード・ライブラリをSchemeから簡単に利用することができる

以上の特徴からCalyst SchemeはJupyter NotebookになれたPythonエンジニアにとっては非常にとっつきやすく使いやすい環境と考えられます。

# インストール方法
Calysto Schemeのインストールは簡単で、python3とpip3が利用できる状態で以下のコマンドを実行するだけです。私の場合はWindows上のWSL2で環境を作成しました。
```bash
$ pip3 install notebook calysto-scheme
```
この記事で紹介するスクリプトを実行するには、NumPyとOpenCVもインストールしておきましょう。
```bash
$ pip3 install numpy opencv-python
```

# Jupyter Notebookの使い方
```bash
$ jupyter notebook
```
でJupyter Notebookを立ち上げてブラウザでアクセスします。通常のNotebookを作成するのと同じくNewボタンを押すと”Calysto Scheme 3”を選択することができ、Calysto SchemeをカーネルとするNotebookを作成することができます。

![01-01_jupyter起動.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/acdbd89e-8812-a7d8-4e56-61a8dabf6a05.png)

Notebookの使い方は通常のPythonカーネルの場合と同じです。

1セルずつにSchemeコードを記述し、結果を確認することができます。SICPでは演習問題を解いていくことになりますが、1問ごとにノートブックを作成すると良いかと思います。

作成したipynbファイルはそのままGitHubにアップすることができ、学習成果を記録したり共有することができます。

ipynbファイルにはコード以外にもMarkdownでコメントを書くことができるので、SICPの演習問題の解答で必要な数式もLaTeX形式で記述することができ、それをそのままGithubにアップすることもできます。
![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/36471c9a-ab9f-8f88-1619-28c94bbcf914.png)


# Visual Studio Codeでの利用
Jupyter Notebookでの利用はブラウザから簡単に行うことができますが、ブラウザ上のJupyter Notebookではカッコの対応付けを確認することができず、カッコを多く記述する必要のあるSchemeのコーディングは少し大変です。

私はその対策としてVS CodeでJupyter Notebookを利用しています。VS CodeにJupyter拡張があるので、これをインストールすることでCalysto SchemeをVS Code上で利用することができます。

![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/ee8f79b7-388d-559b-b0f8-1438ebce8e91.png)

Jupyter拡張をインストール後にipynb拡張子のファイルを作成して開きます。デフォルトではカーネルがPythonになっているので右上のボタンをクリックしてカーネルをCalysto Schemeに変更しましょう。

![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/664dc8af-42e0-e264-5ed6-8997562b5347.png)

あとは、各セルにSchemeのコードを記述して実行していくだけです。下の動画のようにカッコの対応付けを確認できたり、自動インデントもできかつインデントもわかりやすいのでSchemeのプログラムを書くのがだいぶ楽になりました。
![image10.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/8ec12966-3342-fc62-a813-5eecd9fff6c5.gif)

Schemeはスペース2個のインデントで記述することが多いため、以下の設定をワークスペースの設定ファイル`.vscode/settings.json`に記述することをおすすめします。
```json
{
    "editor.tabSize": 2
}
```

# Pythonライブラリの利用
Calysto Schemeの特徴としてPythonのライブラリを利用することができます。例えば、自分の実装した三角関数の数値計算が正しいか確認したい場合に、mathモジュールを`import`して値を比較することができます。
```scheme
(import "math")
(math.sin (/ math.pi 2) )
```
    Out : 1

`import`以外にも、`import-as`や`import-from`という関数も用意されていて、Pythonライブラリを柔軟にCalysto Scheme環境にマッピングすることができます。

また、`python-eval`/`python-exec`というPythonコードをそのままCalysto Scheme上で評価/実行できる関数もあります。

# Pythonライブラリのちょっと踏み込んだ利用
`list`関数を使うとリストを生成することができます。ここで生成したlistはPythonの関数の引数として配列の代わりに利用できます。また、`dict`関数を使うとPythonのdict型のオブジェクトを生成することができます。

```scheme
(list 1 2 3)
(dict '((a : 1)(b : 2)))
```

list/dictの要素にアクセスするには`set-item!`/`get-item`を利用します。
これはPythonでa[0]またはa[0] = 1などの操作をすることに対応します。
```scheme
(define l (list 1 2 3))
(get-item l 1)
```
    Out : 2
listは変更不能なのでset-item!を利用することはできません。
```scheme
(define d (dict '((a : 1)(b : 2))))
(get-item d 'a)
```
    Out : 1
```scheme
(set-item! d 'a 2)
(get-item d 'a)
```
    Out : 2

dictの要素を変更できたことが確認できます。

Calysto Schmeには`type`関数や`dir`関数があるのでどんな型の変数であるか確認することもできます。listの属性を見ると配列の代わりになることがもわかります。
```scheme
(type (list 1 2 3))
```
    Out : <class 'calysto_scheme.scheme.cons'>
```scheme
(dir (list 1 2 3))
```
    Out : (__call__ __class__ __delattr__ __dict__ __dir__ __doc__ __eq__ __format__ __ge__ __getattribute__ __getitem__ __gt__ __hash__ __init__ __init_subclass__ __iter__ __le__ __len__ __lt__ __module__ __ne__ __new__ __next__ __reduce__ __reduce_ex__ __repr__ __setattr__ __sizeof__ __str__ __subclasshook__ __weakref__ car cdr next)

次にCalysto Schemeで外部ライブラリを利用した例としてNumPyを扱ってみましょう。
まずはnumpyをimportしてndarrayを生成します。


```scheme
(import-as "numpy" 'np)
(define a (np.array (list '(1 2) '(3 4))) )
a
```
    Out : array([[1, 2],
                 [3, 4]])

無事にndarrayを作成することができました。Jupyter NotebookでPythonを利用した場合と同じndarrayの結果が表示されるところが嬉しいです。

Calysto Schemeでは`.`を利用することでPythonオブジェクトの属性やメソッドを呼び出すこともできます。
```scheme
a.shape
```
    Out : (2, 2)
```scheme
; a.reshape((4, 1))   # in Python
(a.reshape (list 4 1))
```
    Out : array([[1],
                 [2],
                 [3],
                 [4]])

さらにもう一つのndarrayを生成してみます。
```scheme
; b = np.array([[1, 2, 3], [4, 5, 6]]) # in Python
(define b (np.array (list '(1 2 3) '(4 5 6))) )
b
```
    Out : array([[1, 2, 3],
                 [4, 5, 6]])

最後に2つの行列の積を計算してみます。
```scheme
; np.dot(a, b)   # in Python
(np.dot a b)
```
    Out : array([[ 9, 12, 15],
                 [19, 26, 33]])
正しく行列の積が計算できています。

# 図形言語
私は画像認識エンジニアとして普段Numpy+OpenCVを使ったプログラミングを行っています。日々画像を扱うエンジニアとしてSICP 2.2.4の図形言語（Picture Language）はちょうどNuPy+OpenCVが活用できそうだったので取り組んでみいました。ここではPythonで基本的な関数を実装し、Schemeでそれを組み合わせてました。

せっかくSICPを勉強しているのでPythonの実装も関数型プログラミングっぽくしてみようと思います。painterは、**ndarray型のframeを引数として受け取ってそこに図形を描画する関数（関数オブジェクト）**、として定義しました。

まずは基本のpainterをPythonで実装していきます。`image_painter`は任意の画像ファイルからpainter関数を生成する関数として定義しました。関数を返す関数というところが関数型プログラミングっぽいですね。また`wave`は線分を描画する関数として実装しました。

以下多用する`python-exec`関数はCalysto Scheme上でPythonコードを実行することができる関数で、これを利用してPythonの関数を定義することができます。定義した関数はScheme環境にマッピングされるのでそのままSchemeから呼び出すことができます。pyファイルにこれらの関数を定義してimportすることもできますが、`python-exec`を利用すると1つのipynbファイルにすべてを記述できるので便利です。

```scheme
(python-exec
"
import cv2
import numpy as np
from IPython.display import Image
"
)
```
```scheme
(python-exec
"
def image_painter(image_path):
  image = cv2.imread(image_path)
  def painter_func(frame):
    h = frame.shape[0]
    w = frame.shape[1]
    resized_image = cv2.resize(image, (w, h))
    frame[:,:,:] = resized_image
  return painter_func

def wave(frame):
  segments = (
    (0.000, 0.355, 0.154, 0.590),
    (0.154, 0.590, 0.302, 0.420),
    (0.302, 0.420, 0.354, 0.510),
    (0.354, 0.510, 0.245, 1.000),
    (0.419, 1.000, 0.497, 0.830),
    (0.497, 0.830, 0.575, 1.000),
    (0.748, 1.000, 0.605, 0.540),
    (0.605, 0.540, 1.000, 0.860),
    (1.000, 0.646, 0.748, 0.350),
    (0.748, 0.350, 0.582, 0.350),
    (0.582, 0.350, 0.640, 0.150),
    (0.640, 0.150, 0.575, 0.000),
    (0.419, 0.000, 0.354, 0.150),
    (0.354, 0.150, 0.411, 0.350),
    (0.411, 0.350, 0.285, 0.350),
    (0.285, 0.350, 0.154, 0.400),
    (0.154, 0.400, 0.000, 0.150),
  )
  h = frame.shape[0]
  w = frame.shape[1]
  frame.fill(255)
  for segment in segments:
    x0 = round(segment[0] * w)
    y0 = round(segment[1] * h)
    x1 = round(segment[2] * w)
    y1 = round(segment[3] * h)
    cv2.line(frame, (x0, y0), (x1, y1), (0, 0, 0), 1)
")
```

次に、演算子として`beside`、`below`、`flip-vert`、`flip-horiz`をPythonで実装しました。painter関数を受け取って新たなpainter関数を返す関数として実装しています。

SICPによると`transform-painter`を実装すれば前述の演算子も定義できるのですが、今回は手抜きをしてそれぞれの演算子を実装しました。`flip-vert`/`flip-horiz`はPythonの名前で"-"が使えないのでScheme側でdefineしています。

```scheme
(python-exec
"
def beside(painter1, painter2):
  def painter_func(frame):
      h = frame.shape[0]
      w = frame.shape[1]
      w1 = w // 2
      w2 = w - w1
      subframe1 = np.ndarray((h, w1 ,3))
      subframe2 = np.ndarray((h, w2 ,3))
      painter1(subframe1)
      painter2(subframe2)
      frame[:, :w1, :] = subframe1[:,:,:]
      frame[:, w1:, :] = subframe2[:,:,:]
  return painter_func

def below(painter1, painter2):
  def painter_func(frame):
      h = frame.shape[0]
      w = frame.shape[1]
      h1 = h // 2
      h2 = h - h1
      subframe1 = np.ndarray((h1, w ,3))
      subframe2 = np.ndarray((h2, w ,3))
      painter1(subframe1)
      painter2(subframe2)
      frame[h1:, :, :] = subframe1[:,:,:]
      frame[:h1, :, :] = subframe2[:,:,:]
  return painter_func

def flip_vert(painter):
  def painter_func(frame):
    original_frame = np.ndarray(frame.shape)
    painter(original_frame)
    fliped_frame = np.flipud(original_frame)
    frame[:,:,:] = fliped_frame[:,:,:]
  return painter_func

def flip_horiz(painter):
  def painter_func(frame):
    original_frame = np.ndarray(frame.shape)
    painter(original_frame)
    fliped_frame = np.fliplr(original_frame)
    frame[:,:,:] = fliped_frame[:,:,:]
  return painter_func
"
)

; define alias
(define flip-vert flip_vert)
(define flip-horiz flip_horiz)
```

最後に`paint`という、Jupyter Notebook上で画像を表示する関数も実装します。
```scheme
(python-exec
"
def create_display_image(img):
    decoded_img = cv2.imencode('.jpg', img)[1]
    display_image = IPython.display.Image(data=decoded_img)
    return display_image

def paint(painter):
    frame = np.ndarray((512, 512, 3))
    painter(frame)
    return create_display_image(frame)
")
```

ここまでで基本的な部品が揃ったので、あとはSchemeでどんどん図形を定義していきます。

まずは、Rogers先生とSafieロゴのpainterを定義します。
```scheme
(define rogers (image_painter "rogers.png"))
(define safie (image_painter "safie.png"))
```
paintが適切に動作することをまずは確認してみます。
```scheme
(paint wave)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/2afeacfb-cac8-bf98-a4d3-57eef6336d44.png)
```
(paint rogers)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/aed0f990-5b02-ba02-b60c-11abc8f95ea1.png)

複合演算子として`right-split`、`up-slipt`、`corner-split`を定義します。

```scheme
(define (right-split painter n)
  (if (= n 0)
      painter
      (let ((smaller (right-split painter (- n 1))))
        (beside painter (below smaller smaller)))))
 
(define (up-split painter n)
  (if (= n 0)
      painter
      (let ((smaller (up-split painter (- n 1))))
        (below painter (beside smaller smaller) ))))
 
 
(define (corner-split painter n)
  (if (= n 0)
      painter
      (let ((up (up-split painter (- n 1)))
            (right (right-split painter (- n 1))))
        (let ((top-left (beside up up))
              (bottom-right (below right right))
              (corner (corner-split painter (- n 1))))
          (beside (below painter top-left)
                  (below bottom-right corner))))))
```

最後にこれらの図形を描画をしてみます。
```scheme
(paint (right-split safie 4))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/2dbbb6d7-872a-7269-ac18-1da6df193069.png)
```scheme
(paint (corner-split safie 4))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157307/4be8db98-2647-986d-0942-9e70bac71a70.png)

無事にCalysto Schemeで図形言語が動作することが確認できました。

部品となる関数をPythonで実装して、それをSchemeで組み立てていくというのはなかなかおもしろかったです。Python言語自体がSchemeなどの関数型言語から影響を受けており、2つの言語の親和性も大変良く、連携も簡単に行えました。

# まとめ
Jupyter Notebookの上で動作するScheme処理系のCalysto Schemeを紹介し、Visual Studio Codeでの利用方法やPythonライブラリの利用方法について説明を行いました。

今回作製したNotebookは以下のGitHubで公開しました。
https://github.com/onixwr/calysto_scheme_example

# 参考情報
- Calysto Schemeに関する情報
    - [Calysto Schemeのリファレンスガイド［公式（英語）］](https://nbviewer.org/github/Calysto/calysto_scheme/blob/master/notebooks/Reference%20Guide%20for%20Calysto%20Scheme.ipynb)
    - [Calysto Schemeの言語詳細［公式（英語）］](https://nbviewer.org/github/Calysto/calysto_scheme/blob/master/notebooks/Calysto%20Scheme%20Language.ipynb)
    - [JupyterでScheme処理系を動かしてSICPを勉強する](https://dev.classmethod.jp/articles/sicp-study-on-jupyter/)

- SICPのウェブ公開版
    - [SICP Web Site（英語）](https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/index.html)
    - [計算機プログラムの構造と解釈 第二版（和田 英一氏版）](https://sicp.iijlab.net/)
    - [非公式PDF版SICP（minghai氏版）](https://github.com/minghai/sicp-pdf)
    - [非公式PDF版SICP・新訳（鍋宏史氏版）](https://takeda25.hatenablog.jp/entry/20151030/1446174031)