経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

株式会社いい生活のインターンに参加した

株式会社いい生活|不動産テック・不動産賃貸管理システムをクラウド・SaaSで提供インターンに参加しました。
テクノロジー×不動産領域というのに興味を持ったのと、自社サービスのAPIを叩けるのが楽しそうだったので応募しました。

参加決定するまで

Vue.jsとFirebaseを使うことは知っていたので少し準備をしました。
Vue.jsとFirebaseを業務で使った経験はなかったですが、 本を一冊やったり、ポートフォリオや個人開発で使っていたのである程度慣れていました。
GitHub - wafuwafu13/Introduction-to-Vue.js: Vue.js入門 基礎から実践アプリケーション開発まで
GitHub - wafuwafu13/Practice-TypeScript: 実践TypeScript ~ BFFとNext.js&Nuxt.jsの型定義~
GitHub - wafuwafu13/portfolio: My Portfolio Site / Vue + Vuetify + Typescript + Netlify
GitHub - wafuwafu13/Word-Creater: Create Word file from HTML / Vue.js + Express + Python
インターン参加の直前には、思い出すためにYouTubeチュートリアルを途中までしました。
GitHub - wafuwafu13/Vue.js-Vuetify-Firebase-FULL-PROJECT: Vue.js-Vuetify-Firebase-FULL-PROJECT


選考では面接が一回ありました。
深い技術の話は出てこず、普段していることや、「この会社は何をしているか知ってる?」という質問や逆質問を滞りなく話していたら受かりました。

スケジュール

1日目にGitやVueの講義、ペルソナと機能の設定、2~4日目に実装、5日目に発表資料作成して発表、という流れでした。
講義があることは知らず、TSやNuxt使ったり、5日間徹夜する温度感も想定して準備はしていたのですが、チーム開発がはじめての方や、Vueをはじめて触る方も気軽に参加できる感じでした。
開発体制は、3~4人のチームが3組あり、それぞれペルソナと機能を考えて実装、という形でした。
「いい物件One」などのサービスに使われている「dejima」というAPIのSwaggerをもとに、必要な情報を取り出して表示する、というのが主な開発の流れでした。
僕たちのチームは、大学生をペルソナにし、賃金にフォーカスを当てた機能を実装することにしました。

実装した機能

Vuexの導入や、Community Geocoderを用いて大学の住所から緯度を算出する処理をしました。
無料で利用できてオープンソースのジオコーディング API (住所から緯度経度を検索)「Community Geocoder」を公開しました。 - Geolonia blog
特に注力したのは、お気に入り画面の実装です。
既存の不動産のサイトのお気に入り画面にはあまりない、インタラクティブな要素を盛り込むことを意識しました。

一つ目の機能として、「ユーザーがお気に入り登録した物件の家賃が、ユーザーが求める家賃の範囲内か」が、カードの色で分かる、という機能を実装しました。

お気に入り登録した物件の家賃の最大値と最小値を取得し、その値をVuetifyのスライダーのrangeに設定しました。
Vuetify — A Material Design Framework for Vue.js
そして、ユーザーが設定するrangeに合わせて、家賃が範囲内にあったら青、なかったら赤、というようにclassを動的に変えることにより実現しました。
クラスとスタイルのバインディング — Vue.js


二つ目の機能として、「ユーザーがお気に入り登録した物件を、ドラッグアンドドロップで動かせる」という機能を実装しました。
これは、タイムラインのように、過去に登録したものが下に溜まっていくと、見返す機会が減って比較がしにくくなることを解消したかったのと、技術的に挑戦したかったので実装することにしました。

ドラッグアンドドロップは、Vue.Draggableというライブラリを用いて実現しました。
GitHub - SortableJS/Vue.Draggable: Vue drag-and-drop component based on Sortable.js
苦労したのは、それぞれの物件の位置情報を保持することです。
まずは、カラムを区別するため、 draggableコンポーネントに、draggable1などの固有のclass名を設定しました。
[参考] 
【Vue.js】Vue.Draggableを使って躓いたところ - ROXX開発者ブログ

<div id="box1" class="box">
      <v-layout justify-center>
        <v-card class="mr-12 mt-5">
          <v-card-text>第1候補群</v-card-text>
        </v-card>
      </v-layout>
      <v-col 
       cols="12"
       v-for="(building, i) in buildingData1"
       :key=i>
        <draggable :options="{ group: 'items' }" @end="draggableEnd" class="draggable1">
          <v-layout justify-center>
            <mypage-bukken :building="building" :range="range" :costFlag="costFlag" />
          </v-layout>
        </draggable>
     </v-col>
</div>

次に、ドラッグが終了した時点で発火するdraggableEnd メソッドで、各カラムのDOMをとってきて、そこから物件名を上から順に取得し、Firebase Realtime Databaseに保存する、という処理を行いました。

draggableEnd: async function() {
      const firstColumns = document.getElementsByClassName('draggable1');
      let buildingName = [];
      for (let i = 0; i < firstColumns.length; i++) {
        let buildingHTMLCollection = firstColumns[i].getElementsByClassName('v-card__text');
        if (buildingHTMLCollection.length != 0) {
          for (let i = 0; i < buildingHTMLCollection.length; i += 2) {
            buildingName.push(buildingHTMLCollection[i].innerHTML)
          }
        } 
      }
      const uid = this.$store.state.apiServices.firebaseAuthService.currentUserId();
      await this.$store.state.apiServices.firebaseService.database
        .ref(`users/${uid}/mypage/colomn1`)
        .set({
          buildingName
        })

    .....

保存した位置情報を、マウント時にとってきて復元したかったのですが、「お気に入り登録が一つもないとき」「お気に入り登録はあるが一度もドラッグされていないとき」「お気に入り登録ボタンから新しく登録されたとき」.... といった条件分岐や、カラム の振り分け、APIからまた家賃や画像を取ってくる、といった処理が結構複雑で、工数的に厳しく、少しバグが残った状態で終わってしまいました。
けどFirebase Realtime DatabaseをRealtimeで使えている感触がつかめたので満足です。

感想

「周りの学生にボコボコにされる」という目的は果たせなかったですが、オライリーとにらめっこする日々が続いていたこともあり、チームでワイワイ開発するのは気分転換になって楽しかったです。
また、短時間で、ある程度技術的に困難な実装を設定して実行できたので満足です。
「東京でいい生活をする」というのも一つの目的としてあったのですが、オンラインだったので果たせなかったのは残念でした。
昼休憩の後に営業の方や、カスタマーサポートの方とエンジニアの方の仲介をする方の話を聞けたのはよかったです。
不動産にもう少し詳しかったら、もっとアイデアが出たり楽しみ方も変わってきていたと思ったので、もっと幅広く深い知識をつけていこう、という動機にもなりました。
とりあえず積読してあるFPや簿記の本を消化していこうと思います。
関わってくださった社員の方々、インターン生の方々、ありがとうございました!

『Go言語で作るインタプリタ』をTypeScriptで実装する(前編)

後編はこちら
wafuwafu13.hatenadiary.com


O'Reilly Japan - Go言語でつくるインタプリタ
を2章までTypeScriptで実装しました。

この本を手にした動機は、

  • Goの文法を学習したついでに低レイヤーにも手を出したかった
  • 分量的に手が出やすそうだった

TypeScriptで実装を始めた動機は、

といったところです。

let文の解析を中心としてまとめていきます。
リポジトリはこちらです。
GitHub - wafuwafu13/Interpreter-made-in-TypeScript: Interpreter Written in TypeScript


環境構築

TypeScript webpack Jest ESLint Prettier を用いました。
first commit · wafuwafu13/Interpreter-made-in-TypeScript@313d7a0 · GitHub

字句解析器(レキサー)

ソースコードを入力として受け取り、出力としてそのソースコードを表現するトークン列を返します。
GoのStructJavaScriptclassに置き換えるとうまくいきました。

...
type Lexer struct {
	input        string 
	position     int
	readPosition int
	ch           byte
}

func New(input string) *Lexer {
	l := &Lexer{input: input}
	l.readChar()
	return l
}

func (l *Lexer) readChar() {
	if l.readPosition >= len(l.input) {
		l.ch = 0
	} else {
		l.ch = l.input[l.readPosition]
	}
	l.position = l.readPosition
	l.readPosition += 1
}
......


...
export interface LexerProps {
  input: string;
  position: number;
  readPosition: number;
  ch: string | number;
}

export class Lexer<T extends LexerProps> {
  input: T['input'];
  position: T['position'];
  readPosition: T['readPosition'];
  ch: T['ch'];

  constructor(
    input: T['input'],
    position: T['position'] = 0,
    readPosition: T['readPosition'] = 0,
    ch: T['ch'] = '',
  ) {
    this.input = input;
    this.position = position;
    this.readPosition = readPosition;
    this.ch = ch;

    this.readChar();
  }

  readChar(): void {
    if (this.readPosition >= this.input.length) {
      this.ch = 'EOF';
    } else {
      this.ch = this.input[this.readPosition];
    }
    this.position = this.readPosition;
    this.readPosition += 1;
  }
...

抽象構文木(AST)

ソースコードの内部表現として使われています。
GoではValueフィールドの型はExpressionですが、TypeScriptではNameフィールドと同様にIdentifierクラスの型を用いました。

...
type Node interface {
	TokenLiteral() string
	String() string
}

type Statement interface {
	Node
	statementNode()
}

type Expression interface {
	Node
	expressionNode()
}

type Program struct {
	Statements []Statement
}

func (p *Program) TokenLiteral() string {
	if len(p.Statements) > 0 {
		return p.Statements[0].TokenLiteral()
	} else {
		return ""
	}
}

func (p *Program) String() string {
	var out bytes.Buffer

	for _, s := range p.Statements {
		out.WriteString(s.String())
	}

	return out.String()
}

type LetStatement struct {
	Token token.Token
	Name  *Identifier
	Value Expression
}

func (ls *LetStatement) statementNode() {}

func (ls *LetStatement) TokenLiteral() string { return ls.Token.Literal }

func (ls *LetStatement) String() string {
	var out bytes.Buffer

	out.WriteString(ls.TokenLiteral() + " ")
	out.WriteString(ls.Name.String())
	out.WriteString(" = ")

	if ls.Value != nil {
		out.WriteString(ls.Value.String())
	}

	out.WriteString(";")

	return out.String()
}

type Identifier struct {
	Token token.Token
	Value string
}

func (i *Identifier) expressionNode() {}
func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
func (i *Identifier) String() string { return i.Value }
...


...
export interface ProgramProps {
  statements:
    | LetStatement<LetStatementProps>[]
    | ReturnStatement<ReturnStatementProps>[]
    | ExpressionStatement<ExpressionStatementProps>[];
}

export class Program<T extends ProgramProps> {
  statements: T['statements'];

  constructor(statements: T['statements'] = []) {
    this.statements = statements;
  }
}

export interface LetStatementProps {
  token: Token<TokenProps>;
  name: Identifier<IdentifierProps>;
  value?: Identifier<IdentifierProps>;
}

export class LetStatement<T extends LetStatementProps> {
  token: T['token'];
  name?: T['name'];
  value?: T['value'];

  constructor(token: T['token']) {
    this.token = token;
  }

  tokenLiteral(): string | number {
    return this.token.literal;
  }

  string(): string {
    let statements = [];
    statements.push(this.tokenLiteral() + ' ');
    statements.push(this.name!.string());
    statements.push(' = ');

    if (this.value != null) {
      statements.push(this.value.string());
    }

    statements.push(';');

    return statements.join('');
  }
}

export interface IdentifierProps {
  token: Token<TokenProps>;
  value: string | number;
}

export class Identifier<T extends IdentifierProps> {
  token: T['token'];
  value: T['value'];

  constructor(token: T['token'], value: T['value']) {
    this.token = token;
    this.value = value;
  }

  tokenLiteral(): string | number {
    return this.token.literal;
  }

  string(): string | number {
    return this.value;
  }
}
...

構文解析器(パーサー)

入力データを受け取り、抽象構文木のデータ構造を構築します。
Type Guardが煩わしかったので、新たにDEFAULTトークンを定義しました。
DEFAULTトークンを定義 · wafuwafu13/Interpreter-made-in-TypeScript@42f923e · GitHub


...
func (p *Parser) parseLetStatement() *ast.LetStatement {
	stmt := &ast.LetStatement{Token: p.curToken}

	if !p.expectPeek(token.IDENT) {
		return nil
	}

	stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}

	if !p.expectPeek(token.ASSIGN) {
		return nil
	}

	p.nextToken()

	stmt.Value = p.parseExpression(LOWEST)

	if p.peekTokenIs(token.SEMICOLON) {
		p.nextToken()
	}

	return stmt
}
...

...
parseLetStatement(): LetStatement<LetStatementProps> {
    const stmt: LetStatement<LetStatementProps> = new LetStatement(
      this.curToken,
    );

    if (!this.expectPeek(TokenDef.IDENT)) {
      return stmt;
    }

    stmt.name = new Identifier(this.curToken, this.curToken.literal);

    if (!this.expectPeek(TokenDef.ASSIGN)) {
      return stmt;
    }

    while (!this.curTokenIs(TokenDef.SEMICOLON)) {
      this.nextToken();
    }

    return stmt;
  }
...

テスト

Jestを用いました。
TDDを実践できたのでよかったです。

...
func TestLetStatements(t *testing.T) {
	tests := []struct {
		input              string
		expectedIdentifier string
		expectedValue      interface{}
	}{
		{"let x = 5;", "x", 5},
		{"let y = true;", "y", true},
		{"let foobar = y;", "foobar", "y"},
	}

	for _, tt := range tests {
		l := lexer.New(tt.input)
		p := New(l)
		program := p.ParseProgram()
		checkParserErrors(t, p)

		if len(program.Statements) != 1 {
			t.Fatalf("program.Statements does not contain 1 statements. got=%d",
				len(program.Statements))
		}

		stmt := program.Statements[0]
		if !testLetStatement(t, stmt, tt.expectedIdentifier) {
			return
		}

		val := stmt.(*ast.LetStatement).Value
		if !testLiteralExpression(t, val, tt.expectedValue) {
			return
		}
	}
}
...

...
describe('testLetStatement', () => {
  const tests = [
    { input: 'let x = 5;', expectedIdentifier: 'x', expectedValue: 5 },
    { input: 'let y = true;', expectedIdentifier: 'y', expectedValue: true },
    {
      input: 'let foobar = y;',
      expectedIdentifier: 'foobar',
      expectedValue: 'y',
    },
  ];

  for (const test of tests) {
    const l = new Lexer(test['input']);
    const p = new Parser(l);

    const program: Program<ProgramProps> = p.parseProgram();

    it('checkParserErrros', () => {
      const errors = p.Errors();
      if (errors.length != 0) {
        for (let i = 0; i < errors.length; i++) {
          console.log('parser error: %s', errors[i]);
        }
      }
      expect(errors.length).toBe(0);
    });

    it('parseProgram', () => {
      expect(program).not.toBe(null);
      expect(program.statements.length).toBe(1);
    });

    const stmt: LetStatement<LetStatementProps> | any = program.statements[0];

    it('letStatement', () => {
      expect(stmt.token.literal).toBe('let');
      expect(stmt.name.value).toBe(test['expectedIdentifier']);
      expect(stmt.value.value).toBe(test['expectedValue']);
    });
  }
});
...

デバッグ

Goに不慣れなため、デバッグの仕方が悪かっただけかもしれませんが、TypeScriptだと、きれいにASTの構造がデバッグできました。

Go
TypeScript

特にif文なんかでは、ASTのノードがネストされている構造が掴めたのでよかったです。

if文

感想

低レイヤーの知識はまだまだ浅いですが、入り口としてこの本に出会えたのはよかったです。
現段階ではインタプリタよりむしろ、GoとTypeScriptの文法の知識の方が身についている感覚があるので、また気が向いたら続きをしていこうと思います。
インタプリタの挙動なんて知らなくてもエンジニアにはなれると思いますが、本能的に知りたくなる、周りのみんな知っている世界線にいたい、ということでこれからも独学でやっていくと思います。