ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

「関数型プログラミングの基礎」を読んだ

関数型プログラミングの基礎」を読みました。読んだと言いつつ、まだ理解が薄い部分もあり、要再読という状態です。

http://www.ric.co.jp/book/contents/book_1059.html

再読する前ですが、短い読書記録代わりの感想などを書いてみます。

感想

こちらの本ですが、サブタイトルにもあるように、JavaScriptを使って関数型について書いてあります。
JavaScriptを使ったことがある人なら、関数型を意識せずともつかっている「いつもの」書き方もいくつか載っていました。
そういう面では、(私のような)関数型の初心者にとっても取っつきやすいという印象でした。

内容としても、関数型を使うメリットやカリー化、モナドなどについて分かりやすく書かれていると思います。
モナドについては、以下のサイトも同時に参照して理解を補足させて頂きました(といつつこちらも要再読です)。

30分でわかるJavaScriptプログラマのためのモナド入門

JavaScript(やTypeScript)は最近だとフロント側だけでなく、AWS CDKなどインフラ側でも使うことが多くなっているように思えます。
例えばAWS CDKで、関数型を使ってチェーンでインフラ構造を表すことが出来たら面白いかも・・・、とかちょっと妄想してみました。

ただ関数型のメリットを引き出しつつ使いこなせるようになるには、本書を参照しつつガッツリと何かを作ってみる必要があるかなとも思いました。

AWS Amplifyのローカルモックを動かしてみる

AWS Amplifyのローカルモックを、以下の公式の記事を参考に動かしてみました。
新機能 – Amplify CLI を使用したローカルモックとテスト | Amazon Web Services ブログ

基本的には公式の手順通りにできたのですが、Amplifyを使い慣れてない場合、少し調べながら進めたところもありました。
その辺りも追加しながら、やったことを書いてみたいと思います。

やったこと

1. Amplify CLIのインストール

以下のコマンドでAmplify CLIをインストールします(インストールしていない場合ですが)。
公式ではこのあと「$ amplify configure」でIAMユーザの作成やクレデンシャルの用意を行いますが
既にAWS CLIなどでローカルにクレデンシャルがある場合は不要でした。

$ npm install -g @aws-amplify/cli

2. プロジェクト作成

「$ npx create-react-app~」でプロジェクトを作るのですが、私の環境では「create-react-app」が既にグローバルインストールされていたため、以下のようになりました。

これについては表示されたとおり、一旦グローバルの「create-react-app」はアンインストールしてから進めます。

$ npx create-react-app refillapp

You are running `create-react-app` 4.0.1, which is behind the latest release (4.0.3).

We no longer support global installation of Create React App.

Please remove any global installs with one of the following commands:
- npm uninstall -g create-react-app
- yarn global remove create-react-app

$ npm uninstall -g create-react-app
$ npx create-react-app refillapp
$ cd refillapp

3. APIの初期化

公式に書いてあるコマンドを使い、APIを作成します。
AWSのマネージメントコンソールを開くと、CloudFormationが作成・実行され、Amplifyのアプリが作られていることが分かるかと思います。

$ amplify init

4. APIの作成

以下のコマンドでAPIを作成するのですが、選択肢が幾つかでるので迷いました。
以下の別の公式チュートリアルと同じように入力していくことで今回は進めました。
Tutorial - Connect API and database to the app - Amplify Docs

以下は私が入力したコマンドの一部です。最後の「? Choose your default editor」で選択したエディタが開くので
スキーマを書いて保存します。

$ amplify add api
・・・
? Do you have an annotated GraphQL schema? No
? Do you want to edit the schema now? (y/N) Yes
? Choose your default editor: (Use arrow keys)
・・・

5. ローカルモックの実行

ローカルモックを以下のコマンドで実行します。

$ amplify mock

AppSync Mock endpoint is running at http://xxxxxx

エンドポイントのURLが表示されるので、ブラウザで開いてみます。今回はデータの作成、取得を以下のように確認してみました。
画面左下の「Add new」から「Mutaion」を選び、データを作成するクエリを実行します。データが作成されると、そのidが画面右に表示されます。
次は画面左下の「Add new」から「Query」を選び、先のidを指定して取得してみます。先に登録したデータが表示されていれば大丈夫です。

6. Amplifyの削除

「$ amplify init」の時に、マネージメントコンソールでCloudFormation、Amplifyのアプリが作成されていることを確認しました。これらを最後に削除します。とはいっても、以下のコマンドを実行するだけです。

$ amplify delete

コマンドの実行が終わったら、マネージメントコンソールより削除されていることを確認します。

Athenaのunnestでjson配列を分解したら便利だった

はい。タイトル通りなのですが、Athenaでjsonの配列を扱うときに
unnestで分解したら便利だったので、備忘録として残しておきたいと思います。

まあ他にも同様の記事はあるので、そちらも参考にしてみてください。

以下のようなjson形式のデータが、S3などに入っているとします。
(実際のデータじゃないので適当なサンプルですが)

{"name": "test1", "ary": [{"key": "key1", "value", "value1"}, {"key": "key2", "value", "value2"}, {"key": "key3", "value", "value3"}]

一つのjsonの中に「ary」のような配列形式の要素を持っている場合、unnestを使うと
これを以下のように分割して表示することができます。

name key value
test1 key1 value1
test1 key2 value2
test1 key3 value3

3つの要素がある「ary」を分解して、3行で表示しています。
この分解にunnestを使うのですが、以下のようなクエリとなります。
(元データを参照するテーブル名は「table1」とします)

SELECT
    name,
    json_extract(ary_unnest, '$.key') AS key, --3.
    json_extract(ary_unnest, '$.value') AS value --3.
FROM
(
    SELECT
        name,
        CAST(ary AS array(json)) AS ary_json -- 1.
    FROM
        table1,
        unnest(ary_json) AS t(ary_unnest) -- 2.
)

クエリにはポイントとなる所に番号をコメントしておきました。それぞれの簡単な説明を書くと

1.「ary」をjson配列の型にcastする
2. castしたjson配列をunnsetで分割して「ary_unnest」と別名をつける
3. 分割した「ary_unnest」から、配列内の要素の「key」「value」の値を抽出する

となります。

実際に書いたクエリを見ながら書いているわけではないので、そのままでは動かないかもしれませんが
ポイントとしては大体こんな感じです。

詳細などは(私も参考にした)以下の公式の記事を参考にしてください。

ネストされた配列のフラット化 - Amazon Athena

Three.jsをTypeScriptと組み合わせて学んでみた

ブラウザ側のJavaScriptGPUに命令して3Dを表示できるThree.js というライブラリがあります。
Three.js – JavaScript 3D Library

このThree.jsをTypeScriptで動かしながら、いくつかのサイトを写経してみました。
今回はその時に参考にしたサイトと、JavaScriptで書かれているThree.jsの記事をTypeScriptのプロジェクトで書き直した時のメモを上げておきたいと思います。

Three.js + TypeScriptの環境について

環境については以下のサイトを参考にし、Three.js + TypeScrpt + Webpackという組み合わせとしました。
最新版TypeScript+webpack 5の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き) - ICS MEDIA

Three.jsについて

以下のサイトをTypeScriptに変換しつつ写経しました。

Three.js入門サイト - ICS MEDIA
three.js超入門 第0回 3Dコンピュータグラフィックスの基礎 - Qiita

また他の例などが知りたければ、以下の公式サイトのサンプルを参考になりました。

https://threejs.org/examples/#webgl_geometry_convex

いくつか出たエラーについて

Access to image at 'xxx.jpg' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

CORSエラーですね。とりあえずローカルで簡易的なWebサーバを動かせばよかったので、今回は「goexec」を使いました。
Golangの開発環境がある場合ですが

go get -u github.com/shurcooL/goexec

で「goexec」をインストールし、以下のコマンドを開きたいHTMLファイルがあるフォルダで実行してWebサーバを起動しました。

$GOPATH/bin/goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'

Three.jsのバージョン

Three.jsのバージョンを最新ではなく任意のバージョンにしたい場合などがあります。
以下のコマンドでnpmでインストール可能なバージョンを確認し、package.jsonに記述しました。

npm info three versions

Three.jsの参照

Webpackで必要なモジュールを纏めているので、ビルド前の.htmlファイルからはライブラリの参照の記述を削除しました。

OrbitControls

TypeScript用の型定義である@types がないOrbitControlsのモジュールを直接読み込むため以下を行いました。

npm install --save three-orbitcontrols

でインストールし、以下をmain.tsに記述します。

declare function require(x: string): any;
const OrbitControls = require('three-orbitcontrols');


以上です。次にまた自分がThree.jsを始めるためのメモとして残しておきます。

Shellで複数の結果をそのままforループさせる

普段はあまりシェルを書かないので手続き的に書こうとしてハマったのでメモ代わりに書いておきます。
タイトルにあるように、あるコマンドを実行して結果が複数返ってくるとします。
この結果をループして別処理を呼び出す引数にしようとしたら、思ったよりハマってしまいました。

結論としては、
1.あるコマンドを実行する
2.その1.の結果を配列などの変数に保持する
3.変数をループする

とするのではなく、

1.あるコマンドを実行する
2.それをそのままfor~inに渡す

とすれば良かったようです。
ソースを見た方が早いので以下にサンプルを上げます。

サンプル

以下のようなjsonをjqで解析して「name」の数だけループして値を別処理の引数とするとします。

sample.json

{
    "name": "name1",
    "description": "description1"
}
{
    "name": "name2",
    "description": "description2"
}
{
    "name": "name3",
    "description": "description3"
}

シェルは以下のようになりました。

sample.sh

#!/bin/sh

for value in $(cat sample.json | jq .name)
do
    echo "name = "$value
done

実行すると以下のように「name」の値を取得して別処理(今回はecho)に渡していることが分かります。

$ ./sample.sh
name = "name1"
name = "name2"
name = "name3"

まとめ

分かってしまえば簡単なのですが、jqの結果をそのままfor~inに渡すというのを直ぐに思いつきませんでした。
将来の自分や誰かの役に立てば幸いです。

追記

以下のように「declare -a」を使って最初に考えた結果を配列などの変数に保持する方法もできるようです。

sample2.sh

#!/bin/bash

declare -a ary=$(cat sample.json | jq .name)
for value in $ary
do
    echo $value
done

はじめてのFlutter - Drawer、List、State を使ってみる

タイトルにもあるようにFlutterを触り始めました。

Install - Flutter
こちらに「Get Started」のインストールから始め、公式サイトのチュートリアルをいくつかやってみた後で、理解を深めるために以下のようなサンプルをつくってみました。

右下の「+」を押すとカウントが増える
f:id:UnderSourceCode:20210124121434p:plain

上の画面の左上をタップすると Drawer が開く
f:id:UnderSourceCode:20210124122248p:plain

「Item List」をタップするとリストが表示され、「+」を押した数だけ「Item N」が増える
f:id:UnderSourceCode:20210124122355p:plain

これらはFlutterのプロジェクトを「New Application Project」から作成した雛形と、以下の「Add Drawer」のチュートリアルにあるソースを合体させたものに手を加えてつくりました。
Add a Drawer to a screen - Flutter

タイトルにあるようにFlutterのDrawer、List、Stateを使っています。以下にソースを書いておきます。

私自身は公式のチュートリアルをやった後に以下を実装してみました。公式チュートリアルをやったあと、以下の例など色んなサンプルを動かしてみることで理解が早まる気がします。

ソースとDrawer、List、Stateについて

ソースは分かりやすいよう「lib/main.dart」に纏めてみました。ローカルで試す場合もプロジェクトを作成して以下のソースを張り付ければ動くかと思います(バージョン変更などなければですが・・・)

lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Drawer List Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  List<String> _listItems = new List<String>();

  void _incrementCounter() {
    setState(() {
      _counter++;
      _listItems.add('Item $_counter');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      // add drawer
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: _createDrawerList(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

  List<Widget> _createDrawerList() {
    return <Widget>[
      DrawerHeader(
        child: Text('Drawer Header',
            style: TextStyle(color: Colors.white, fontSize: 20)),
        decoration: BoxDecoration(
          color: Colors.blue,
        ),
      ),
      ListTile(
        leading: Icon(Icons.home),
        title: Text('Home'),
        onTap: () {
          Navigator.pop(context);
        },
      ),
      ListTile(
        leading: Icon(Icons.list),
        title: Text('Item List'),
        onTap: () {
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => ListScreen(listItems: _listItems),
            ),
          );
        },
      ),
    ];
  }
}

class DetailScreen extends StatefulWidget {
  final DetailRecord detailRecord;

  DetailScreen({Key key, @required this.detailRecord}) : super(key: key);

  @override
  _DetailScreenState createState() => _DetailScreenState();
}

class _DetailScreenState extends State<DetailScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.detailRecord.title),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(widget.detailRecord.text),
      ),
    );
  }
}

class DetailRecord {
  String title;
  String text;
  DetailRecord(String title, String text) {
    this.title = title;
    this.text = text;
  }
}

class ListScreen extends StatefulWidget {
  final List<String> listItems;

  ListScreen({Key key, @required this.listItems}) : super(key: key);

  @override
  _ListScreenState createState() => _ListScreenState();
}

class _ListScreenState extends State<ListScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Item List'),
      ),
      body: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return Container(
              decoration: BoxDecoration(
                border: Border(
                  bottom: BorderSide(color: Colors.black38),
                ),
              ),
              child: ListTile(
                leading: const Icon(Icons.info),
                title: Text(widget.listItems[index]),
                onTap: () {
                  DetailRecord detailRecord =
                      new DetailRecord("Item Detail", widget.listItems[index]);
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) =>
                          DetailScreen(detailRecord: detailRecord),
                    ),
                  );
                },
              ));
        },
        itemCount: widget.listItems.length,
      ),
    );
  }
}
Drawer

メインとなる最初の画面を作る「_MyHomePageState」クラスの「build」メソッドにて、文字通り「drawer」について記述している箇所があります。その中で「_createDrawerList」というWidgetのListを作成するメソッドを呼び出しています。

この部分がDrawerを作っている所となります。物凄い大雑把な説明になってしまうのですが、要は複数のWidgetをListで集める形でDrawerを作っているような感じです。

List

「+」をタップした数の「Item N」を表示するリストは、「ListScreen」クラスとその状態を保持する「_ListScreenState」クラスで構成しています。リストに表示する複数の値は「listItems」というString型のListとして保持しています。この「listItems」をリストを表示する「ListView.builder」で使うことでリスト形式での表示を行っています。

「listItems」は「+」をタップするごとに数が増えるのですが、それについては次の「State」で触れます。ちなみにリストで表示される「Item N」をタップすると簡単な詳細画面に遷移するようにもしてあります。ソースを動かせる方はやってみてください。

State

State = 状態ですが、今回のように「+」をタップする度に動的に増える値などはStateとして保持します。「+」ボタンはメインとなる最初の画面の「_MyHomePageState」クラスの「build」メソッドにて「floatingActionButton」の箇所にて定義しています。

この「_MyHomePageState」クラスですが、Stateを保持するよう「class _MyHomePageState extends State」のように「State」を継承して作られています。

「+」をタップした数とリストに表示する値は、Staetを継承した「_MyHomePageState」クラス内のメンバ変数「_counter」「_listItems」で保持します。「+」をタップした時に呼ばれる「_incrementCounter」メソッドを見ると「setState」内で「_counter」「_listItems」がそれぞれ増えている or 追加していることが分かるかと思います。

先にリストを表示する「ListScreen」クラスを説明したところで「listItems」というString型のListを表示していると書きましたが、「builder: (context) => ListScreen(listItems: _listItems)」の箇所でStateとして保持していた「_listItems」を「ListScreen」クラスに渡しています。

Golangのx.text.transformが便利だった

タイトルにある様にGolangのx.text.transformパッケージについてです。
このパッケージを使うと、readerやwriter内のデータを変換することができます。
良くある用途としては、readerで読み込んだ文字列の一部を変換して出力する、などがあると思います。

このパッケージを知った切っ掛けは以下のSlideShareです。
オススメの標準・準標準パッケージ20選

SlideShareで言及されていますが、パッケージのドキュメントは以下となり、Exampleを見れば使い方は分かるかと思います。
https://godoc.org/golang.org/x/text/transform#Transformer
https://godoc.org/github.com/tenntenn/text/transform#example-ReplaceTable

transformパッケージ自体については上記のリンクを見れば分かるのですが
自分が理解するために書いたサンプルソースもメモ代わりに上げておきます。

Golangで2つのgzファイルを連結してみる - ソースコードから理解する技術-UnderSourceCode
以前書いた上記のソースを改修したもので、2つの圧縮ファイル(中身はjson)を読み込み、値を変換して、圧縮ファイルに出力するものです。

サンプルソース

「sample1.json.gz」「sample2.json.gz」というgzファイルをあらかじめ用意しておき、実行すると中身が連結されて「result.json.gz」というファイルに出力されます。

sample1、sample2のjsonの中身は以下のような形式で、「abc」「fgh」をそれぞれ大文字に変換して出力しています。

sample1

{
    "data":"abcde"
}

sample2

{
    "data":"fghij"
}

メインの処理は以下のようになります。

package main

import (
	"compress/gzip"
	"fmt"
	"io"
	"log"
	"os"

	. "github.com/tenntenn/text/transform"
	"golang.org/x/text/transform"
)

func write(br1 io.Reader, br2 io.Reader) error {
	writeFile, err := os.OpenFile("./result.json.gz", os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	defer writeFile.Close()

	zw := gzip.NewWriter(writeFile)
	defer zw.Close()

	_, err = io.Copy(zw, br1)
	if err != nil {
		return err
	}

	_, err = zw.Write([]byte("\n"))
	if err != nil {
		return err
	}

	_, err = io.Copy(zw, br2)
	if err != nil {
		return err
	}

	return nil
}

func doTransform(zr *gzip.Reader, old, new []byte) io.Reader {
	t := ReplaceByteTable{
		old, new,
	}
	return transform.NewReader(zr, ReplaceAll(t))
}

func main() {
	readFile1, err := os.Open("./sample1.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile1.Close()

	zr1, err := gzip.NewReader(readFile1)
	if err != nil {
		log.Fatal(err)
	}
	defer zr1.Close()

	readFile2, err := os.Open("./sample2.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile2.Close()

	zr2, err := gzip.NewReader(readFile2)
	if err != nil {
		log.Fatal(err)
	}
	defer zr2.Close()

	tr1 := doTransform(zr1, []byte(`abc`), []byte(`ABC`))
	tr2 := doTransform(zr2, []byte(`fgh`), []byte(`FGH`))

	err = write(tr1, tr2)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("-----finish-----")
}

transformパッケージを使っているのはdoTransform()メソッドになります。
.gzファイルを読み込んだreaderを受け取り、変換するルールをReplaceByteTableに定義し、新たに別のreaderを生成して返却しています。

返却したreaderはwrite()に渡し、そのままresult.json.gzに出力しています。

このような感じにreaderの中身を変換して出力できることが確認できました。