この記事はジーズアカデミー Advent Calendar 2019の22日目の記事です。
・マイナビIT AGENT
迷ったらまずはココ。求人数が多いため希望や条件にマッチする求人が見つかる!
・レバテックキャリア
担当者のレベルが高い。エンジニア特化で15年の実績がある実績で初回提案での内定率驚異の90%!
・JOIN
副業やフリーランスとして高額報酬案件を見つけるならJOIN。未経験者でも案件獲得実績豊富が強み!
プログラミング翻訳サービス、ProgLearn
みなさん、こんにちは。どんぶラッコです。
上にも記載しましたが、この記事は起業家養成スクール G’s Academy のアドベントカレンダーです。私はTokyoのLAB6期生として半年間お世話になりました。
このスクールでは、自分が作成したサービスを発表するGlobal Geek Auditionという機会があるのですが、その時に、私、面白いサービス(自分で言った!)を発表しました。
それは、プログラミングの翻訳サービスです。
…どういうこと?
はい、その疑問ごもっともです。
では、実際にご覧ください。

画面の左側にJavaScriptを入力してEnterキーを押すと…

\ッターン/

さあ、右側にご注目です。


翻訳されてるーーー!
こんなサービスを粛々と作っています。夢が広がりますね。
このサービスを説明するとギークな方々から「どうやって作ってるの?」という質問が飛んできます。
この翻訳ロジックを作る部分、実は結構苦労したんです。
ということで、今回は
- プロトタイプNo.1 の翻訳ロジック
- No.1 の反省と プロトタイプNo.2
についてご紹介します!
プロトタイプ No.1
一番最初は自分でなんちゃってMVCフレームワークを作ってアプリを構築しました。

翻訳についてはGrammerModel
内に定義しています。
そして肝心の翻訳ロジックですが、
- 空白 or 改行でsplit、
;
で一区切りとみなし、多重配列を作成する - 多重配列の先頭を予約語とみなし、ルールを定義する
というものでした。
なので、例えば
let i = 0;
という文言があったなら、
['let', 'i', '=', 0, ';']
という形に分解してから、先頭の予約語(let
)に基づいて翻訳を実行する、というものでした。

ちなみに、関数名が和訳されているのは、関数名を単語ごとに分解してGoogle翻訳にぶん投げています。
No.1 の反省と プロトタイプNo.2
さて、上記のようにゴリ押しで実装を進めていた私ですが、実装ロジックを改めて作り直すことにしました。
その理由は、上記のロジックでは立ち行かないケースが多数出てきてしまったためです。
例えば、for文。
for文は
for (let i =0; i<10; i++){
...
}
のように記述しますが、先ほどの
- 空白 or 改行でsplit、
;
で一区切りとみなし、多重配列を作成する - 多重配列の先頭を予約語とみなし、ルールを定義する
に基づいて分解を実施すると
[ ['for', '(', 'let', 'i', '=', 0, ';'], ['i', '<', 10, ';'], ...]
このように、for文がバラバラになってしまいます。
そこで、
- 接頭辞がforだった場合、
()
を一つの配列で扱う
という例外設定を追加しなければなりません。
しかし同様の事象はif文でも関数でも同様に発生してしまいます。
つまり、例外設定が多く生まれてしまうということは、ルールとして機能していないということに他なりません。
また、さらに更に悪いことに、splitの条件が”半角スペースがあること”としています。
つまり、
i=0
のような書き方をされてしまった場合、
['i=0']
と数式の状態のまま配列化されてしまいます。
そこで、
- JavaScriptを意味ごとの塊で分類出来ること
を条件に、新たなルール探しを模索していました。
そんな私の前に現れたのが、Babelライブラリです。
この技術の詳細については、別のAdvent Calendarでも書かせてもらいました。
BabelはBabel Toolingという形でBabelにまつわる機能を提供しています。
その中にある、parse
という機能がまさに私が追い求めていた機能を提供していました。
例えばこのプログラム
function isSendSalt(person){
return person==='上杉謙信'
}
if(isSendSalt('上杉謙信')){
console.log('武田信玄「ありがとう」')
}
これを、Babelではこのように分解してくれます。
Node {
type: 'File',
start: 0,
end: 111,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 7, column: 1 } },
errors: [],
program:
Node {
type: 'Program',
start: 0,
end: 111,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'script',
interpreter: null,
body: [
Node {
type: 'FunctionDeclaration',
start: 0,
end: 55,
loc: SourceLocation { start: [Position], end: [Position] },
id:
Node {
type: 'Identifier',
start: 9,
end: 19,
loc: [SourceLocation],
name: 'isSendSalt' },
generator: false,
async: false,
params: [ [Node] ],
body:
Node {
type: 'BlockStatement',
start: 27,
end: 55,
loc: [SourceLocation],
body: [Array],
directives: [] } },
Node {
type: 'IfStatement',
start: 57,
end: 111,
loc: SourceLocation { start: [Position], end: [Position] },
test:
Node {
type: 'CallExpression',
start: 60,
end: 78,
loc: [SourceLocation],
callee: [Node],
arguments: [Array] },
consequent:
Node {
type: 'BlockStatement',
start: 79,
end: 111,
loc: [SourceLocation],
body: [Array],
directives: [] },
alternate: null
} ],
directives: [] },
comments: [] }
この情報を基に、ルール付けを実施しました。
switch (type) {
case 'BinaryExpression': {
resultObj = toNLang.binaryExpression(props)
break
}
case 'ExpressionStatement': {
resultObj = toNLang.expressionStatement(props)
break
}
case 'FunctionDeclaration': {
resultObj = toNLang.functionDeclaration(props)
break
}
case 'ForStatement' : {
resultObj = toNLang.forStatement(props)
break
}
case 'IfStatement': {
resultObj = toNLang.ifStatement(props)
break
}
case 'ReturnStatement': {
resultObj = toNLang.returnStatement(props)
break
}
case 'VariableDeclaration': {
resultObj = toNLang.variableDeclaration(props)
break
}
....
}
プログラム自体の可読性も上がっています。
まずは作ってみる
一見良さそうに見えるNo.2の方針ですが、これもまだ不完全であると考えています。
それは、結局ルールの個別設定になってしまっている点です。なので、この部分のルール化・改良化に取り組もうと思っています。
ロジックを考えながら実感したのは、まずは動いてみることの重要性。
一度作ってみると、新しい発見や気づきを得ることができました。今回の場合も1つ目のプロトタイプを作っていなかったら、翻訳ルールの穴に気づくことができなかったと思います。
その意味で、高速でプロトタイプを作り、実験をするという精神を持って、これからもプロダクト開発に携わっていきたいと思います!