Lambda で Scheme を実行する

[AWS Lambda Custom Runtimes芸人 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/lambda-custom-runtimes) の 7日目です。

AWS Lambda で [Custom Runtime がサポート](https://aws.amazon.com/jp/blogs/aws/new-for-aws-lambda-use-any-programming-language-and-share-common-components/) され、任意の言語を Lambda 上で実行できるようになりました。

λλλλλλλλ… Lambda といったらやっぱり Lisp ですよね。
というわけで、Lisp方言の１つである Scheme の実装である Racket を Lambda で動かしてみました。

Custom Runtime の仕組みは [こちら](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-custom.html)

## Runtime を実装する

ソースコードは [GitHub](https://github.com/ojima-h/lambda-scheme-runtime) にあげてあります。

Runtime 本体の実装はこんな感じです。

```racket:runtime.rkt
(require json)
(require net/http-client)

;;;
;;; 環境変数から必要な情報を取得する.
;;;
;;; _HANDLER 変数からファイル名とHandler関数名を取り出す.
;;;
(define-values (filename handler-name)
  (let* ([_handler (getenv "_HANDLER")]
         [hs (string-split _handler ":")])
    (values (string-join (drop-right hs 1) ":")
            (string->symbol (last hs)))))
(define lambda_task_root (getenv "LAMBDA_TASK_ROOT"))
(define-values (api host port)
  (let* ([_api (getenv "AWS_LAMBDA_RUNTIME_API")]
         [ps (string-split _api ":")])
    (values _api (car ps) (string->number (cadr ps)))))

;;;
;;; Handler関数を取り出す
;;;
(define handler
  (let ([ns (make-base-namespace)])
    (parameterize ([current-namespace ns])
      (namespace-require 'racket ns)
      (load filename))
    (namespace-variable-value handler-name #t #f ns)))

;;;
;;; Handler　関数を呼び出す.
;;;
;;; 1. Custom Runtime APIからEvent情報を取得する
;;; 2. Handler関数にEventオブジェクトを渡して呼び出す
;;; 3. 結果を Custom Runtime APIに渡す
;;; 4. 以上を繰り返し
;;;
(let loop ()
  (define-values (status _headers body)
    (http-sendrecv
      host
      (string-append "http://" api "/2018-06-01/runtime/invocation/next")
      #:port port))

  (define headers
    (make-hash
      (map
        (lambda (v)
          (let* ([vs (string-split (bytes->string/utf-8 v) ":" #:trim? #f)]
                 [key (string-trim (car vs))]
                 [val (string-trim (string-join (cdr vs) ":"))])
            (cons key val)))
        _headers)))
  (define requet-id (hash-ref headers "Lambda-Runtime-Aws-Request-Id"))

  (define event (read-json body))
  (define context (hasheq))

  (define response (handler event context))

  (http-sendrecv
    host
    (string-append "http://" api "/2018-06-01/runtime/invocation/" requet-id "/response")
    #:port port
    #:data
      (cond
        [(or (hash? response) (list? response))
          (with-output-to-string (lambda () (write-json response)))]

        [(void? response) ""]

        [else (~a response)])
    #:method "POST")

  (loop))
```

Runtimeスクリプトを呼び出すための `bootstrap` スクリプトも用意します。

```sh:bootstrap.sh
#!/bin/sh

runtime_dir=$(dirname $0)
cd ${LAMBDA_TASK_ROOT}
${runtime_dir}/racket/bin/racket -f ${runtime_dir}/runtime.rkt
```

`runtime.rkt`, `bootstrap.sh` と Racket 処理系をまとめてzipパッケージを作成します。

```sh
mkdir -p build

# dowload Racket
curl -o /tmp/racket.sh https://mirror.racket-lang.org/installers/7.1/racket-minimal-7.1-x86_64-linux.sh
echo yes | sh /tmp/racket.sh --in-place --dest build/racket

# add runtime.rkt and bootstrap
(cd build && zip ../runtime.zip -q -r .)
chmod +x bootstrap
zip runtime.zip runtime.rkt bootstrap
```

この zip パッケージを Lambda Layer としてデプロイします。
AWS CLI やコンソールからもできるのですが、ここでは AWS SAM テンプレートから作ってみます。

```yml:template.yml
---
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:
  SampleFunction:
    Type: 'AWS::Serverless::LayerVersion'
    Properties:
      LayerName: scheme-runtime
      Description: scheme (racket) runtime
      ContentUri: runtime.zip
      LicenseInfo: 'Available under the LGPL license.'
```

```sh
aws cloudformation package \
  --template-file template.yml \
  --output-template-file packaged-template.yml \
  --s3-bucket ${S3_BUCKET}
aws cloudformation deploy \
  --template-file packaged-template.yml \
  --stack-name ${STACK_NAME}
```

以上で、Lambda から Scheme スクリプトを実行する準備ができました。

## Lambda 関数を用意する

Scheme といったらやっぱり SICP ですよね。

SICP の練習問題を Lambda で実行してみます。

> https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-24.html#%_sec_3.5

> Exercise 3.69.
> Write a procedure triples that takes three infinite streams, S, T, and U, and
> produces the stream of triples (Si,Tj,Uk) such that i < j < k. Use triples to
> generate the stream of all Pythagorean triples of positive integers, i.e.,
> the triples (i,j,k) such that i < j and i2 + j2 = k2.

遅延ストリームを使ってピタゴラス数を探そうという問題です。

コードを書きます。

```racket:function.rkt
(require json)

(define integers
  (stream-cons 0 (stream-map (lambda (x) (+ x 1)) integers)))
(define integers+ (stream-rest integers))

(define (interleave s1 s2)
  (if (stream-empty? s1)
      s2
      (stream-cons (stream-first s1)
                   (interleave s2 (stream-rest s1)))))

(define (pairs s t)
  (stream-cons
   (list (stream-first s) (stream-first t))
   (interleave
    (stream-map (lambda (x) (list (stream-first s) x))
                (stream-rest t))
    (pairs (stream-rest s) (stream-rest t)))))

(define (triples s t u)
  (let ((ss (stream-map (lambda (x) (cons (stream-first s) x)) (pairs t u))))
    (stream-cons
      (stream-first ss)
      (interleave
        (stream-rest ss)
        (triples (stream-rest s) (stream-rest t) (stream-rest u))))))

(define pythagorean-triples
  (stream-filter
    (lambda (x) (= (+ (expt (car x) 2)
                      (expt (cadr x) 2))
                   (expt (caddr x) 2)))
    (triples
      integers+
      integers+
      integers+)))

(define (handler event context)
  (let* ((n (string->number (hash-ref (hash-ref event 'queryStringParameters) 'length)))
         (response (hash 'result (stream->list (stream-take pythagorean-triples n)))))
    (hash
      'headers (hash 'Content-Type "application/json")
      'body (with-output-to-string (lambda () (write-json response))))))
```

`handler` 関数の中で、Eventオブジェクトから探索したいピタゴラス数の組み合わせの数を取り出して、計算結果を返しています。

これも、AWS SAM テンプレートから実行してみます。

```yml:template.yml
---
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:
  SampleFunction:
    Type: 'AWS::Serverless::Function'
    Properties:
      FunctionName: scheme-sample
      Handler: function.rkt:handler
      Runtime: provided
      CodeUri: .
      MemorySize: 1024
      Timeout: 600
      Layers:
        - <Lambda Layer ARN>
      Events:
        API:
          Type: Api
          Properties:
            Path: /pythagorean-triples
            Method: get
```

```sh
aws cloudformation package \
  --template-file template.yml \
  --output-template-file packaged-template.yml \
  --s3-bucket ${S3_BUCKET}
aws cloudformation deploy \
  --template-file packaged-template.yml \
  --capabilities CAPABILITY_IAM \
  --stack-name ${STACK_NAME}
```

試しに実行してみましょう。

```sh
curl https://API_GATEWAY_ENDPOINT/Prod/pythagorean-triples?length=3

#=> {"result":[[3,4,5],[6,8,10],[5,12,13]]}
```

これで、Lambda で SICP できるようになりました。
サーバー管理が不要で、無限にスケールし、使った分だけ課金の、最強のサーバーレス SICP です :innocent: 

## おわりに

Scheme Runtime の残念な点として、ちゃんと使える AWS SDK がほとんどありません。
（というか、デフォルトで提供されていない言語で公式にAWS SDKが提供されているものは PHP と C++ くらいです）

AWS サービスにまともにアクセスできない Lambda 関数をわざわざ使う状況というのはかなり限られそうです。

ただ、特定の用途に特化した言語をピンポイントで使えるのは面白そうな気がします。
基本的には Java で、絶対にバグを出したくないAPIは Haskell で、数値計算したいところは Python で、SICP したいところは Scheme で、とかとか…

今後どんな事例が出てくるのが楽しみです :smile: