2025/10/02 Omotesando.rb メモ

mruby型検査

  • ruby-ti
    • vim専用
    • 型の情報を補助する目的
      • ちょっと色を薄くして断定はしない
  • mruby
    • 小さいから
    • 組み込み用

感想

  • mrubyがあまりわからないが、仕様が小さいところから始めるというのは納得感あった

管理画面のフロントエンド

  • Vue
    • 読みやすく開発体験は悪くない
  • 管理画面にVueを混入させたくないという意向
    • Hotwire
    • ユーザー向けはReact
  • Hotwire
    • 結構古いJSの記述
    • 採用は厳しい

感想

  • Hotwire触ってみたい
  • 似たような問題はどこでもぶつかる
    • 現職でも管理画面にVueはあるが、更新に手が回らずリプレイス対象となっている

Concerns再考

  • Concern好きじゃない
  • Before Concernは難しい
  • After Concernはすっきりした
  • Fat modelはConcernによって解決できるかは曖昧
    • コードは分割するが責務はそうでもないかも
    • privateメソッドもincludeするので責務も巻き込むのでは?
  • AttachableやSearchableは個人の経験に依存しているのでわかりづらいのでは?
    • ちゃんと汎用的に作ろう

感想

  • Concernはあまり使いたくないな
  • 横断的な関心に特化すべきだけど特定の用途に限定されているケースが多いと、委譲や合成でいいのではと思っている
  • 共通化・抽象化が難しいのは同意

本番環境で安全にmigration

  • mysql8系でchange column typeもオンラインでいけるらしい
    • 安全ではない
    • AddCheckConstraint
    • RemoveColumn
      • ignore_columnが必要
  • 知らない人が踏んでしまう
  • strong_migrations gem
    • 全部が回避できるではない
    • exampleがあるので参考にすればいい

感想

  • 身に覚えがありすぎる
    • 現職で直近に同様のインシデントがあり、strong_migrationsは検討にあがっていた
  • 誰かがstrong_migrationsあまり入れたくないと言っていて理由が気になった

【React】ドラッグ&ドロップされたPNG画像を編集して表示する

目的

  • PNG画像を編集したい
  • 画像はドラッグ&ドロップで受け付ける
  • 受け付けた画像は編集して表示されたものを保存する

使用したパッケージ

  • react
  • react-dropzone
  • pngjs

コード

  • 表示する画像のデータはbase64で渡す
'use client'

import { useDropzone } from "react-dropzone"
import { PNG } from 'pngjs/browser'
import { useState } from 'react'

export default function Home() {
  const onDrop = (files) => {
    setUploadedFiles(uploadedFiles.concat(files))
    files.forEach(file => {
      const fileReader = new FileReader()
      fileReader.addEventListener('loadend', (event) => {
        const png = new PNG({filterType: 4}).parse(event.target.result, (error, image) => {
          if (error) {
            console.error(error)
            return
          }

          const newPng = new PNG({width: png.width, height: png.height})
          // newPng.dataを操作して画像を編集

          // base64にしてstateに入れる
          const base64 = PNG.sync.write(newPng).toString('base64')
          const imageData = {
            base64: base64,
            width: newPng.width,
            height: newPng.height
          }
          setImages(images.concat(imageData))
        })
      })
      fileReader.readAsArrayBuffer(file)
    })
  }

  const { getRootProps, getInputProps } = useDropzone({ onDrop })
  const [ uploadedFiles, setUploadedFiles ] = useState([])
  const [ images, setImages ] = useState([])

  const renderImage = (image, index) => {
    const dataUri = 'data:image/png;base64,' + image.base64
    const key = `image-${index}`

    return (<>
      <div key={key}>
        <Image src={dataUri} alt='' width={image.width} height={image.height} />
      </div>
    </>)
  }

  return (
    <div>
      <main>
        <div {...getRootProps()} style={{ border: "1px dashed #ff7f7f", width: 400, height: 150 }}>
          <input {...getInputProps()} />
          <p>ここにファイルをドラッグ&ドロップしてください。</p>
        </div>
        <div>
          {images.map(renderImage)}
        </div>
      </main>
    </div>
  );
}

React.js & Next.jsの勉強メモ

概要

React.js & Next.js超入門 第2版 を読んでちまちま写経していたが、 firebaseのところが現行のversion9とは結構乖離が大きくなっていたので、差分をメモしておく。 (買ってから1年以上放置してたのがそもそも悪い)

差分

初期化

書籍内

import firebase from 'firebase'

const firebaseConfig = {..}
if ( firebase.apps.length == 0) {
  firebase.initializeApp(firebaseConfig)
}

現行

import { initializeApp } from 'firebase/app';

const firebaseConfig = {...}
const app = initializeApp(firebaseConfig);
export default app;

firestoreオブジェクトの取得

書籍内

import firebase from 'firebase'

const db = firebase.firestore()

現行

import { getFirestore } from 'firebase/firestore';

const db = getFirestore(app)

ドキュメントの取得(複数)

書籍内

db.collection('mydata').get().then((snapshot) => {
  snapshot.forEach((document) => {
    const doc = document.data()
    ...
  })
})

現行

import { getFirestore, collection, getDocs } from 'firebase/firestore';

const cols = collection(db, 'mydata')
getDocs(cols).then((snapshot) => {
  snapshot.forEach((document) => {
    const doc = document.data()
    ...
  })
})

ドキュメントの取得(単一)

書籍内

import firebase from 'firebase'
import { useRouter } from 'next/router'

const router = useRouter()

if ( router.query.id != undefined ) {
  db.collection('mydata').doc(router.query.id).get().then(ob => {
    const data = ob.data()
    ...
  })
}

現行

import { getFirestore, collection, doc, getDoc } from 'firebase/firestore'
import { useRouter } from 'next/router'

const router = useRouter()

if ( router.isReady && router.query.id !== undefined ) {
  const docRef = doc(db, 'mydata', router.query.id)
  getDoc(docRef).then(ob => {
    const data = ob.data()
    ...
  })
}

追加

書籍内

import firebase from 'firebase'

const db = {
  name: name,
  mail: mail,
  age: age
}

db.collection('mydata').add(ob).then(ref => {
  ...
})

現行

import { getFirestore, collection, addDoc } from "firebase/firestore"

const ob = {
  name: name,
  mail: mail,
  age: age
}

addDoc(collection(db, 'mydata'), ob).then(ref => {
  router.push('/fire')
})

削除

書籍内

import firebase from 'firebase'
import { useRouter } from 'next/router'

const router = useRouter()

db.collection('mydata').doc(router.query.id)
  .delete().then(ref => {
    ...
  })

現行

import { getFirestore, doc, getDoc } from 'firebase/firestore'
import { useRouter } from 'next/router'

const router = useRouter()

const docRef = doc(db, 'mydata', router.query.id)
deleteDoc(docRef).then(ref => {
  ...
})

検索

書籍内

import firebase from 'firebase'

// 検索対象の文字列
const name = '...'

// 完全一致
db.collection('mydata').where('name', '==', name).get()
  .then(snapshot => {
    snapshot.forEach((document) => {
      ...
    })
  })

// 前方一致
db.collection('mydata').orderBy('name').startAt(name).endAt(name + "\uf8ff")
  .then(snapshot => {
    snapshot.forEach((document) => {
      ...
    })
  })

現行

import { getFirestore, collection, query, where, orderBy, startAt, endAt, getDocs } from 'firebase/firestore/lite';

// 検索対象の文字列
const name = '...'

// 完全一致
const cols = collection(db, 'mydata')
const q = query(cols, where('name', '==', name))
getDocs(q).then(snapshot => {
  snapshot.forEach(document => {
    ...
  })
})


// 前方一致
const cols = collection(db, 'mydata')
const q = query(cols, orderBy('name'), startAt(name), endAt(name + "\uf8ff"))
getDocs(q).then(snapshot => {
  snapshot.forEach(document => {
    ...
  })
})

ちなみに startAt(name) endAt(name + "\uf8ff") は範囲指定を意味している。 \uf8ff はコードポイントの最大値であり、上記はnameに続く文字が 0x0000〜0xf8ff の範囲にあることを示している。 例えば name: hoge のとき、

  • 先頭4文字が hoge に一致し、
  • 5文字目が 0x0000〜0xf8ff の範囲にある、つまり終端や全ての文字に一致する

という条件になるらしい。

Docker + SolrCloud

概要

以下の書籍の内容をベースにDocker + Solr8でSolrCloud環境を構築した。

構成

  • ZooKeeper
  • Solr
    • シャード 1、レプリカ 2の構成で2つのSolrは同じデータを保持する前提
    • スキーマはAPIで管理するmanaged-schemaではなく自前で管理するschema.xmlを使用
  • Nginx
    • 2つのSolrへリクエストを振り分けるLB
    • 片方が死んでいる場合は生きている方に流す

f:id:sota2502:20200907225046p:plain

コード

https://github.com/sota2502/japan_address

使用するデータ

郵便番号データダウンロード - 日本郵便 を使用した。 全都道府県で約12万件。 Shift-JIS → UTF-8 への変更とスキーマに合わせたヘッダーを追加している。

ken_all.csv

ZooKeeper

いきなりSolrCloudの構成を作る前にZooKeeperに慣れておくとよい。

docker-compose-zk.yml

version: '3.7'

x-zoo-service: &zoo-service
  image: zookeeper:3.5
  restart: always
  networks:
    - solr

services:
  zoo1:
    <<: *zoo-service
    container_name: zoo1
    hostname: zoo1
    ports:
      - 2181:2181
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 1
    volumes:
      - ./data/zoo1/data:/data
      - ./data/zoo1/datalog:/datalog
  zoo2:
    <<: *zoo-service
    container_name: zoo2
    hostname: zoo2
    ports:
      - 2182:2181
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 2
    volumes:
      - ./data/zoo2/data:/data
      - ./data/zoo2/datalog:/datalog
  zoo3:
    <<: *zoo-service
    container_name: zoo3
    hostname: zoo3
    ports:
      - 2183:2181
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 3
    volumes:
      - ./data/zoo3/data:/data
      - ./data/zoo3/datalog:/datalog

networks:
  solr:

各々のZooKeeperのデータを永続化するため、/data,/datalog をVolumeにマウントする。これによりrestartなどをしてもデータは失われない。

ZooKeeperのクラスタを起動。

$ docker-compose -f docker-compose-zk.yml up -d

ZooKeeperのクライアントを用意する。Macであればhomebrewでインストールするのが簡単。

$ brew install zookeeper

zoo1 に接続してznodeを作成。

$ zkCli -server localhost:2181 create /hoge fuga
Connecting to localhost:2181

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
Created /hoge

先ほど作ったznodeのデータを zoo2 から読み出す。

$ zkCli -server localhost:2182 get /hoge
Connecting to localhost:2182

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
fuga

SolrのCollection用Configsetの作成

_default のConfigsetをベースに郵便番号を管理するためのConfigsetを作成する。これはCollectionを作るときに使う。

以下のコマンドを実行して、テンポラリなコンテナを作って、その中から _default Configsetsを取り出す。

$ docker create --name mysolr solr:8.5.2
$ docker cp mysolr:/opt/solr/server/solr/configsets/_default ./configsets
$ docker rm mysolr

この中で solrconfig.xml 、 schema.xml を編集していく。

特別なポイントはUUIDの設定をしているところ。 Solrのドキュメントを更新するためにはUniqueKeyが必要になる。しかし、郵便番号データにはUniqueKeyに適したものが存在しない。(同一の郵便番号の市町村が複数存在するケースがある) この制約はCSVを編集してIDを採番することでも満たすことは可能。 しかし、UUIDを採番するようにしておくほうが手間は省ける。

solrconfig.xml

差分は以下の通り。

38a39
>   <schemaFactory class="ClassicIndexSchemaFactory" />
1111c1112
<   <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:true}"
---
>   <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:false}"
1117a1119,1124
>   <updateRequestProcessorChain name="uuid" default="true">
>     <processor class="solr.UUIDUpdateProcessorFactory">
>       <str name="fieldName">uuid</str>
>     </processor>
>     <processor class="solr.RunUpdateProcessorFactory" />
>   </updateRequestProcessorChain>

<schemaFactory class="ClassicIndexSchemaFactory" /> は managed-schema の代わりに schema.xml を使用する場合に記述する。

add-unknown-fields-to-the-schema の UpdateRequestProcessorChain のdefault値をfalseにしておく。これはschemalessモードを使わないための設定。

uuid の UpdateRequestProcessorChain はUUIDを採番するための設定。

schema.xml

_default Configsetsには schema.xml は存在しない。 これは managed-schema を schema.xml にリネームして手動編集する。 差分は以下のようになる。これは郵便番号データに対応したfieldの定義の追加と、不要なfieldの定義の削除、UniqueKeyの変更をしている。

111a112,127
>     <field name="uuid" type="uuid" multiValued="false" indexed="true" required="true" stored="true"/>
>     <field name="jis" type="string" indexed="true" required="true" stored="true"/>
>     <field name="postcode" type="string" indexed="true" required="true" stored="true"/>
>     <field name="postcode5" type="string" indexed="true" required="true" stored="true"/>
>     <field name="prefecture_kana" type="text_ja" required="true"/>
>     <field name="city_kana" type="text_ja" required="true"/>
>     <field name="town_area_kana" type="text_ja" required="true"/>
>     <field name="prefecture" type="text_ja" indexed="true" required="true" stored="true"/>
>     <field name="city" type="text_ja" indexed="true" required="true" stored="true"/>
>     <field name="town_area" type="text_ja" indexed="true" required="true" stored="true"/>
>     <field name="is_one_town_by_multi_postcode" type="boolean" required="true"/>
>     <field name="is_need_small_area_address" type="boolean" required="true"/>
>     <field name="is_chome" type="boolean" required="true"/>
>     <field name="is_multi_town_by_one_postcode" type="boolean" required="true"/>
>     <field name="updated" type="boolean" required="true"/>
>     <field name="update_reason" type="boolean" required="true"/>
113d128
<     <field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
117,122d131
<     <!-- If you don't use child/nested documents, then you should remove the next two fields:  -->
<     <!-- for nested documents (minimal; points to root document) -->
<     <field name="_root_" type="string" indexed="true" stored="false" docValues="false" />
<     <!-- for nested documents (relationship tracking) -->
<     <field name="_nest_path_" type="_nest_path_" /><fieldType name="_nest_path_" class="solr.NestPathField" />
< 
169c178
<     <uniqueKey>id</uniqueKey>
---
>     <uniqueKey>uuid</uniqueKey>
1012a1022,1023
>     <fieldType name="uuid" class="solr.UUIDField" indexed="true" />
> 
1023d1033
< 

SolrCloud環境の構築

docker-compose で ZooKeeper 、 Solr 、 Nginx のSolrCloud環境を構築する。

docker-compose

docker-compose.yml やその他に必要なファイルは以下のようになる。

docker-compose.yml

version: '3.7'

x-zoo-service: &zoo-service
  image: zookeeper:3.5
  restart: always
  networks:
    - solr

x-solr-service: &solr-service
  image: solr:8.5.2
  environment:
    ZK_HOST: zoo1:2181,zoo2:2181,zoo3:2181
    SOLR_JAVA_MEM: -Xms1g -Xmx1g -Xmn512m
  networks:
    - solr
  depends_on:
    - zoo1
    - zoo2
    - zoo3

services:
  zoo1:
    <<: *zoo-service
    container_name: zoo1
    hostname: zoo1
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 1
    volumes:
      - ./data/zoo1/data:/data
      - ./data/zoo1/datalog:/datalog
  zoo2:
    <<: *zoo-service
    container_name: zoo2
    hostname: zoo2
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 2
    volumes:
      - ./data/zoo2/data:/data
      - ./data/zoo2/datalog:/datalog
  zoo3:
    <<: *zoo-service
    container_name: zoo3
    hostname: zoo3
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 3
    volumes:
      - ./data/zoo3/data:/data
      - ./data/zoo3/datalog:/datalog
  solr1:
    <<: *solr-service
    container_name: solr1
    volumes:
      - ./data/solr1:/var/solr
  solr2:
    <<: *solr-service
    container_name: solr2
    volumes:
      - ./data/solr2:/var/solr
  nginx:
    image: nginx:1.19.2
    container_name: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - 8080:80
    networks:
      - solr
    depends_on:
      - solr1
      - solr2

networks:
  solr:

Solrのデータを永続化するために /var/solr をVolumeにマウントする。

zoo.env

ZOO_SERVERS=server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
ZOO_4LW_COMMANDS_WHITELIST=mntr,conf,ruok

nginx.conf

user nginx;
worker_processes  auto;

error_log /var/log/nginx/error.log warn;
pid       /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {
  upstream solr {
    server solr1:8983;
    server solr2:8983;
  }

  server {
    listen 80;
    client_max_body_size 20M;

    location / {
      proxy_pass http://solr;
    }
  }
}

コンテナを起動。

$ docker-compose up -d

起動が完了したら http://localhost:8080/solr/ にブラウザでアクセスする。SolrのWeb管理画面が表示される。 Cloud → Graph でSolrノードの繋がりが見える。

f:id:sota2502:20200908011613p:plain

ConfigsetsのUpload

[SolrのCollection用Configsetの作成] で作ったConfigsetsをアップロードする。 これは一度zipで一つにしたものをAPIで送る。

$ cd configsets/conf
$ zip -r japan_address.zip *
$ curl -X POST --header "Content-Type:application/octet-stream" \
  --data-binary @japan_address.zip \
  "http://localhost:8080/solr/admin/configs?action=UPLOAD&name=japan_address"

ZooKeeperコンテナに対して2181ポートをマッピングしておいて、 zkCli で ls コマンドを実行するとConfigsetsに対するznodeができているのが確認できる。 (上記のdocker-compose.ymlではポートをマッピングしていないので、これを行う場合には ports の記述を追加する必要がある)

$ zkCli -server localhost:2181 ls /configsets

Collectionの作成

http://localhost:8080/solr/#/~collections:Solr管理コンソール からCollectionを作成する。 今回はシャード1、レプリカ2なので以下のように入力する。

項目 値
name japan_address
configset japan_address
numShards 1
replicationFactor 2

Add Collection をクリックして画面をリロードするとCollectionが作成されたのが確認できる。

CSVデータのインポート

以下のコマンドを実行してCSVデータをインポートする。これは結構時間がかかる。

$ curl -H 'Content-type: text/csv; charset=utf-8' \
  --data-binary @ken_all.csv \
  'http://localhost:8080/solr/japan_address/update?commit=true' 

検索の実行

Curlコマンドか Solr管理画面 でクエリを実行すると結果が返ってくるのが見える。

$ curl 'http://localhost:8080/solr/japan_address/select?q=*:*'

フェールオーバーの確認

現在は2つのSolrにNginxがリクエストを振り分けている状態。 ここから片方のSolrが落ちた状態でもリクエストを処理していることを確認する。

以下のコマンドで solr2 のコンテナを停止させる。

$ docker-compose stop solr2

Solr管理画面のCloud → Tree で片方が停止しているのが把握できる。

Curlで何回かクエリを実行して、いずれもレスポンスが返ってきているのが確認できる。

WWW::Curl::Easyでファイルをアップロード

目的

WWW::Curl::EasyでファイルをPOSTでアップロードしたい。 ただし、ファイルは巨大になるケースを想定して、一旦メモリに読み込むのを避けたい。

コード

use strict;
use warnings;

use WWW::Curl::Easy;

my $file = 'hoge.csv';
my $size = -s $file;

open my $fh, '<', $file or die 'Cannot open file';
my $url = 'http://example.com/sample';

my @headers = (
    "Content-type: text/csv; charset=utf8"
);

my $curl = WWW::Curl::Easy->new();
$curl->setopt(CURLOPT_POST, 1);
$curl->setopt(CURLOPT_URL, $url);
$curl->setopt(CURLOPT_POSTFIELDSIZE_LARGE, $size);
$curl->setopt(CURLOPT_READDATA, \$fh);
$curl->setopt(CURLOPT_HTTPHEADER, \@headers);

if ( $curl->perform != 0) {
    die 'fail to upload';
}

my $code = $curl->getinfo(CURLINFO_HTTP_CODE);
print "Code: $code";

close $fh;

参考

https://stackoverflow.com/questions/9485157/wwwcurl-how-to-upload-post-large-files

YAPC::Tokyo 2019 に参加した感想

とりあえず雑でも感想を書いておく。後にすると絶対書かない。

メッセージングのプロトコル

https://speakerdeck.com/lyokato/yapc-japan-tokyo-2019-metusezingufalsepurotokoru

  • 発表後の本人のMediumのエントリ を読むと発表の意図がわかりやすい。確かに、特定の技術に対して安易にマイナスなイメージをつけないようにされていて、その点はすごくよかった。
  • どうすれば標準化がうまくいって広まったんだろう?慎重になりすぎてたんだろうか?
  • スライドの文字が小さくて読みづらく、マイクが離れていたので聞きづらかったので、情報を得るのがしんどかった。。

ランチセッション Garuda:汎用コネクティビティ プラットフォーム Perlをサイエンスの世界につなぐ

  • 創薬はすごい手間も時間もかかるし、そこに非エンジニアのリソースをバンバン投入したいので、そういうところで重宝されるんだろうな。そういった用途で非エンジニア向けにはすごく需要高そう。

ランチセッション 20歳の僕が経験したPerlエンジニアへの道

  • 「やりたい」「やりたくない」だけで選ばず、やることによる理由をポジティブに捉えてるのがよかった。
  • 知らない言語の現場に放り込まれるというのはきっとあるだろうし、自分の学習プロセスを構築するのにはいい経験。

綱渡りバッチ脱出大作戦

https://speakerdeck.com/shinpeim/gang-du-ribatutituo-chu-da-zuo-zhan

  • 直近で全cronサーバーの入れ替えのため、全バッチが何をやってどういう挙動をしているかを調査しないといけなかったので、苦労にものすごく共感した。。
  • 成否やスケジューリングの管理をしたくてRundeckとか試してたけど、前者だけならhorensoだけで良さそう。後者に関しては冗長化の観点をすっぽり抜けていて「確かに」と思った。
  • 前職もそうだったけど、過度にcronに依存するシステムはつらい。。
  • プレゼンが聞きやすかった。

Wazuhを利用した大統一サーバ監査基盤

https://speakerdeck.com/pyama86/wazuhwoli-yong-sita-da-tong-sabajian-cha-ji-pan

  • セキュリティインシデントの調査は本当にすごく手間だし時間もかかるよね。。
  • またToB要件で顧客情報を扱うケースに遭遇したら入れておきたい

エンジニアリング組織論への招待 -2つのDXと技術的負債-

https://speakerdeck.com/hirokidaichi/2tufalsedxtoji-shu-de-fu-zhai-yapc-tokyo-2019

  • ペアプロよりも要求インスペクションの方が品質改善効果が高いというのは良い知見。
  • 「交換可能であること」が良いことと言語化されていて、すっきりした。

Perlでも分散トレーシングしたい!AWS::XRay による解析とその実装

https://speakerdeck.com/fujiwara3/yapc-tokyo-2019

  • 新規でPerlでサービス作って、マイクロサービスを進めるときには使いたい。
  • 今担当しているサービスはモノリスで、ここからマイクロサービスに切り分けてくこともないので、Devel::KYTProfで十分そう。

Dive into MySQL Error

https://speakerdeck.com/yoku0825/dive-into-mysql-error-yapc-tokyobaziyon

  • 今まで適当にエラーのメッセージやコードを見ていたのが、実際MySQLのソースコード上でどういう風に定義・実装されているのがかわかってよかった。
  • 出力メッセージは割と適当な場合もあるので、正直に捉えないほうがいいこともある。

Erlangことはじめ

Erlangことはじめ

erlangを書くことになったので、初歩的なスクリプトの実行からテストの実行までを勉強。

ディレクトリ構成

myproject
├── ebin
│   └── mymath.beam    # コンパイル時に生成される
├── rebar
├── src
│   └── mymath.erl
└── test
    └── mymath_test.erl

rebarを持ってくる

Erlangのビルドツールであるrebarを持ってくる。コンパイルやテスト実行が簡単になる。

git clone https://github.com/rebar/rebar.git
cd rebar
make

コードを書く

フィボナッチ数、階乗を返す関数を定義したモジュールを書く。

src/mymath.erl

-module(mymath).
-export([fib/1, fact/1]).

fib(X) when X < 0 ->
    throw({error, 'Negative Number'});
fib(0) ->
    1;
fib(1) ->
    1;
fib(X) ->
    fib(X - 1) + fib(X - 2).

fact(X) when X < 0 ->
    throw({error, 'Negative Number'});
fact(0) ->
    0;
fact(1) ->
    1;
fact(X) ->
    X * fact(X - 1).

試しに実行してみる。

$ rebar compile
$ erl -pa ebin
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V6.4  (abort with ^G)
1> mymath:fib(3).
3
2> mymath:fact(3).
6

テストを書く

mymath.erlに対する単体テストを書く。 テストのファイルはmymath_test.erlになる。 テストでは*_testのメソッドが呼ばれる。

test/mymath_test.erl

-module(mymath_test).
-include_lib("eunit/include/eunit.hrl").

fib_test() ->
    ?assertThrow({error, 'Negative Number'}, mymath:fib(-1)),
    ?assertEqual(1, mymath:fib(0)),
    ?assertEqual(1, mymath:fib(1)),
    ?assertEqual(2, mymath:fib(2)),
    ?assertEqual(3, mymath:fib(3)),
    ?assertEqual(5, mymath:fib(4)).

fact_test() ->
    ?assertThrow({error, 'Negative Number'}, mymath:fact(-1)),
    ?assertEqual(0, mymath:fact(0)),
    ?assertEqual(1, mymath:fact(1)),
    ?assertEqual(2, mymath:fact(2)),
    ?assertEqual(6, mymath:fact(3)),
    ?assertEqual(24, mymath:fact(4)).

Eunit には他にもテスト用のメソッドがたくさんある。

テストを実行する

テストを実行してテストが通るか確かめる。

$ ./rebar eunit
==> myproject (eunit)
  All 3 tests passed.

参考