お題
アプリ実行中だけ機能する簡易Key/Valueストアを各プログラミング言語で実装してみる。
(最近、サービス毎に異なるプログラミング言語で実装されている現場に行くことが多いので、使ったことないプログラミング言語にも、それなりに慣れておくため)
お題のツールとしては(もっと便利なものは山ほどあるし)正直なところ日常的に役に立つものとはとても言えないのだけど、(すぐに挫折しないように)ミニマムに始める(けど、少しずつ各プログラミング言語の機能を使っていけるし、要件を拡張していきやすい)には、これくらいがいいかなと。
ちなみに、各言語のバージョンは以下の通り。
現場で使うことはまだなさそうだけど、Rustも加えてもいいかも。
PHPやTypeScriptも考えたけど、コンソールアプリ用途で選定されるだろうか?と疑問に思ったので止めた。
- Ruby ... v2.5
- Golang ... v1.11
- Java ... v1.8
- Scala ... v2.11
- Kotlin ... v1.3
- Python ... v3.6
更新履歴
- 【2019/08/03】コメントにならってRubyプログラムを修正
- 【2019/08/03】コメントでPythonプログラムの事例をもらったので追加
対象読者
JavaやRubyなど1つくらいプログラミング言語をある程度さわっている人。
その中でも、教科書読んだりチュートリアルやったりだといまいち理解できないから、やっぱり何かしらのアプリを実際に作ってみようという人。
注釈
自分としても使ったことないプログラミング言語で実装したりすることから、まずは相当拙いレベルから”とにかく動くものを作る”ことから始めてみる。
(お題の規模だと1ソースファイルで事足りるので、オブジェクト指向言語でも汎用性を考慮してクラス分割などしない。使えそうだとしても関数型の書き方もしない。)
ので、各プログラミング言語を知っている人からすると「こうすべき!」「これはまずい!」といった言いたいことが山ほどあると思うのだけど、そこはグッと堪えてもらいたく。
試行Index
- 第1回:簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ
- 第2回:【改善編】簡単なツール作成を通してRubyを学ぶ
- 第3回:【改善編】簡単なツール作成を通してPython3を学ぶ
- 第4回:【改善編】簡単なツール作成を通してGolangを学ぶ
- 第5回:【改善編】簡単なツール作成を通してJavaを学ぶ
- 第6回:【改善編】簡単なツール作成を通してScalaを学ぶ
- 第7回:簡単なツール作成を通してRustを学ぶ
- 第8回:【改善編】簡単なツール作成を通してRustを学ぶ
実装・動作確認端末
# OS - Linux(Ubuntu)
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
ID_LIKE=debian
実践
要件
アプリを起動すると、起動中だけキーバリュー形式でテキスト情報を保存する機能を持つコンソールアプリ。
以下のように振る舞う。
※「$ 」付きの状態がコンソールでコマンドを叩くことを表す。(無しはアプリ実行中。)
※Unix思想を踏まえ、保存や削除などは正常終了時は何も表示しない。
アプリ起動
$ book
バイナリ化していたら「book
」と叩いて起動する想定だけど、スクリプト言語のケースもあるし、実際のアプリ起動方法はまちまち。
例えばRubyなら「$ ruby main.rb
」だし、Goなら「$ go run main.go
」だし。
アプリ終了
end
$
ヘルプ
help
★ここに、コマンドの一覧や使い方を表示する。★
保存
save key01 val01
正常に保存できた時は何も出力しない。(UNIX Philosophy ?)
取得
get key01
val01
削除
remove key01
一覧表示
list
"key","value"
"key01","val01"
"11111","22222"
"キー","バリュー"
■Ruby
開発環境
# 言語バージョン
$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux-gnu]
# IDE - RubyMine
RubyMine 2019.1.2
Build #RM-191.7141.58, built on May 14, 2019
ソース全量(【2019/08/03】コメントにならって修正)
def usage()
puts <<~EOB
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。
list ... 保存済みの内容を一覧表示します。
save ... keyとvalueを渡して保存します。
get ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
help ... ヘルプ情報(当内容と同じ)を表示します。
EOB
end
# -------------------------------------------------------------------
# ここからメイン処理
# -------------------------------------------------------------------
cmd_store = Hash.new
puts "Start!"
loop do
# 改行コードが含まれるので削る。
cmd, *args = gets.chomp.split /\s+/
case cmd
# アプリ終了判定
when "end"
puts "End!"
exit
# ヘルプ
when "help"
usage
next
# 保存
when "save"
if args.size == 2
cmd_store[args[0]] = args[1]
else
usage
end
# 取得
when "get"
if args.size == 1
puts cmd_store[args[0]]
else
usage
end
# 削除
when "remove"
if args.size == 1
cmd_store.delete(args[0])
else
usage
end
# 一覧
when "list"
puts '"key","value"'
cmd_store.each {|k, v| puts %("#{k}","#{v}")}
end
end
1ファイルだから上に載せたのが全量だけど、一応、ソースは下記に置いている。
https://github.com/sky0621/book_rb/tree/v0.2.0
動作確認
アプリ起動 〜 ヘルプ表示(help) 〜 アプリ終了(end)
$ ruby main.rb
"Start!"
help
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。
list ... 保存済みの内容を一覧表示します。
save ... keyとvalueを渡して保存します。
get ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
help ... ヘルプ情報(当内容と同じ)を表示します。
end
End!
$
アプリ起動 〜 1件追加(save) 〜 追加データ確認(get) 〜 アプリ終了(end)
$ ruby main.rb
"Start!"
save key01 val01
get key01
val01
end
End!
アプリ起動 〜 1件追加(save) 〜 追加データ削除(remove) 〜 追加データ確認(get) 〜 アプリ終了(end)
$ ruby main.rb
"Start!"
save key01 val01
remove key01
get key01
end
End!
アプリ起動 〜 3件追加(save) 〜 データ一覧確認(list) 〜 アプリ終了(end)
$ ruby main.rb
"Start!"
save key01 val01
save 11111 22222
save キー バリュー
list
"key","value"
"key01","val01"
"11111","22222"
"キー","バリュー"
end
End!
■Go
開発環境
# 言語バージョン
$ go version
go version go1.11.4 linux/amd64
# IDE - Goland
GoLand 2019.2
Build #GO-192.5728.103, built on July 23, 2019
ソース全量
package main
import (
"bufio"
"log"
"os"
"strings"
)
func usage() {
msg := `
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。
list ... 保存済みの内容を一覧表示します。
save ... keyとvalueを渡して保存します。
get ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
help ... ヘルプ情報(当内容と同じ)を表示します。
`
println(msg)
}
// -------------------------------------------------------------------
// ここからメイン処理
// -------------------------------------------------------------------
var cmdStore = map[string]string{}
func main() {
println("Start!")
for {
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
// アプリ終了判定
if s.Text() == "end" {
println("End!")
os.Exit(-1)
}
// ヘルプ
if s.Text() == "help" {
usage()
}
if s.Text() == "" {
usage()
continue
}
// 以降は、引数ありコマンドの処理
cmds := strings.Split(s.Text(), " ")
// 保存
if cmds[0] == "save" {
if len(cmds) != 3 {
usage()
continue
}
cmdStore[cmds[1]] = cmds[2]
}
// 取得
if cmds[0] == "get" {
if len(cmds) != 2 {
usage()
continue
}
println(cmdStore[cmds[1]])
}
// 削除
if cmds[0] == "remove" {
if len(cmds) != 2 {
usage()
continue
}
delete(cmdStore, cmds[1])
}
// 一覧
if cmds[0] == "list" {
println(`"key","value"`)
for k, v := range cmdStore {
println("\"" + k + "\",\"" + v + "\"")
}
}
}
if s.Err() != nil {
log.Fatal(s.Err())
}
}
}
1ファイルだから上に載せたのが全量だけど、一応、ソースは下記に置いている。
https://github.com/sky0621/book_go/tree/v0.1.0
■Java
開発環境
# 言語バージョン
$ java -version
openjdk version "1.8.0_202"
OpenJDK Runtime Environment (build 1.8.0_202-20190206132807.buildslave.jdk8u-src-tar--b08)
OpenJDK GraalVM CE 1.0.0-rc14 (build 25.202-b08-jvmci-0.56, mixed mode)
# IDE - IntelliJ IDEA
IntelliJ IDEA 2019.2 (Ultimate Edition)
Build #IU-192.5728.98, built on July 23, 2019
ソース全量
import java.util.*;
public class Main {
private static void usage() {
String msg = "\n" +
"[usage]\n" +
"キーバリュー形式で文字列情報を管理するコマンドです。\n" +
"以下のサブコマンドが利用可能です。\n" +
"\n" +
"list ... 保存済みの内容を一覧表示します。\n" +
"save ... keyとvalueを渡して保存します。\n" +
"get ... keyを渡してvalueを表示します。\n" +
"remove ... keyを渡してvalueを削除します。\n" +
"help ... ヘルプ情報(当内容と同じ)を表示します。\n" +
"\n";
System.out.println(msg);
}
// -------------------------------------------------------------------
// ここからメイン処理
// -------------------------------------------------------------------
private static Map<String, String> cmdStore = new HashMap<String, String>();
public static void main(String... args) {
System.out.println("Start!");
while (true) {
Scanner s = new Scanner(System.in);
String cmd = s.nextLine();
// アプリ終了判定
if (cmd.equals("end")) {
System.out.println("End!");
System.exit(-1);
}
// ヘルプ
if (cmd.equals("help")) {
usage();
continue;
}
if (cmd.equals("")) {
usage();
continue;
}
// 以降は、引数ありコマンドの処理
String[] cmds = cmd.split(" ");
// 保存
if (cmds[0].equals("save")) {
if (cmds.length != 3) {
usage();
continue;
}
cmdStore.put(cmds[1], cmds[2]);
}
// 取得
if (cmds[0].equals("get")) {
if (cmds.length != 2) {
usage();
continue;
}
System.out.println(cmdStore.get(cmds[1]));
}
// 削除
if (cmds[0].equals("remove")) {
if (cmds.length != 2) {
usage();
continue;
}
cmdStore.remove(cmds[1]);
}
// 一覧
if (cmds[0].equals("list")) {
System.out.println("\"key\",\"value\"");
cmdStore.entrySet().stream().map(e -> "\"" + e.getKey() + "\",\"" + e.getValue() + "\"").forEach(System.out::println);
}
}
}
}
1ファイルだから上に載せたのが全量だけど、一応、ソースは下記に置いている。
https://github.com/sky0621/book_java/tree/v0.1.0
■Scala
開発環境
# 言語バージョン
$ scala -version
Scala code runner version 2.11.12 -- Copyright 2002-2017, LAMP/EPFL
# IDE - IntelliJ IDEA
IntelliJ IDEA 2019.2 (Ultimate Edition)
Build #IU-192.5728.98, built on July 23, 2019
ソース全量
object Main extends App {
def usage(): Unit = println(
"""
|[usage]
|キーバリュー形式で文字列情報を管理するコマンドです。
|以下のサブコマンドが利用可能です。
|
|list ... 保存済みの内容を一覧表示します。
|save ... keyとvalueを渡して保存します。
|get ... keyを渡してvalueを表示します。
|remove ... keyを渡してvalueを削除します。
|help ... ヘルプ情報(当内容と同じ)を表示します。
|
|""".stripMargin)
// -------------------------------------------------------------------
// ここからメイン処理
// -------------------------------------------------------------------
import scala.collection.mutable
var cmdStore = mutable.Map.empty[String, String]
println("Start!")
while (true) {
val cmds = io.StdIn.readLine().split(" ")
// アプリ終了判定
if (cmds(0) == "end") {
println("End!")
sys.exit(-1)
}
// ヘルプ
if (cmds(0) == "help") {
usage()
}
if (cmds(0) == "") {
usage()
}
// 保存
if (cmds(0) == "save") {
if (cmds.size != 3) {
usage()
} else {
cmdStore += (cmds(1) -> cmds(2))
}
}
// 取得
if (cmds(0) == "get") {
if (cmds.size != 2) {
usage()
} else {
if (cmdStore.contains(cmds(1))) {
println(cmdStore(cmds(1)))
}
}
}
// 削除
if (cmds(0) == "remove") {
if (cmds.size != 2) {
usage()
} else {
cmdStore -= cmds(1)
}
}
// 一覧
if (cmds(0) == "list") {
println("\"key\",\"value\"")
for ((k, v) <- cmdStore) println("\"%s\",\"%s\"".format(k, v))
}
}
}
1ファイルだから上に載せたのが全量だけど、一応、ソースは下記に置いている。
https://github.com/sky0621/book_scala/tree/v0.1.0
■Kotlin
開発環境
# 言語バージョン
v1.3
# IDE - IntelliJ IDEA
IntelliJ IDEA 2019.2 (Ultimate Edition)
Build #IU-192.5728.98, built on July 23, 2019
ソース全量
import java.util.*
import kotlin.system.exitProcess
fun usage() {
val msg = """
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。
list ... 保存済みの内容を一覧表示します。
save ... keyとvalueを渡して保存します。
get ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
help ... ヘルプ情報(当内容と同じ)を表示します。
""".trimIndent()
println(msg)
}
// -------------------------------------------------------------------
// ここからメイン処理
// -------------------------------------------------------------------
var cmdStore = mutableMapOf<String, String>()
fun main(vararg args: String) {
println("Start!")
while (true) {
val s = Scanner(System.`in`)
val cmd = s.nextLine()
// アプリ終了判定
if (cmd == "end") {
println("End!")
exitProcess(-1)
}
// ヘルプ
if (cmd == "help") {
usage()
continue
}
if (cmd == "") {
usage()
continue
}
// 以降は、引数ありコマンドの処理
val cmds = cmd.split(" ")
// 保存
if (cmds[0] == "save") {
if (cmds.size != 3) {
usage()
continue
}
cmdStore[cmds[1]] = cmds[2]
}
// 取得
if (cmds[0] == "get") {
if (cmds.size != 2) {
usage()
continue
}
println(cmdStore[cmds[1]])
}
// 削除
if (cmds[0] == "remove") {
if (cmds.size != 2) {
usage()
continue
}
cmdStore.remove(cmds[1])
}
// 一覧
if (cmds[0] == "list") {
println("\"key\",\"value\"")
cmdStore.forEach { k, v -> println("\"$k\",\"$v\"") }
}
}
}
1ファイルだから上に載せたのが全量だけど、一応、ソースは下記に置いている。
https://github.com/sky0621/book_kt/tree/v0.1.0
■Python
開発環境
# 言語バージョン
$ python3 --version
Python 3.6.8
# IDE - PyCharm
PyCharm 2019.2 (Professional Edition)
Build #PY-192.5728.105, built on July 24, 2019
ソース全量
def usage():
msg = """
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。
list ... 保存済みの内容を一覧表示します。
save ... keyとvalueを渡して保存します。
get ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
help ... ヘルプ情報(当内容と同じ)を表示します。
"""
print(msg)
# -------------------------------------------------------------------
# ここからメイン処理
# -------------------------------------------------------------------
cmd_store = {}
print("Start!")
while True:
cmd = input()
# アプリ終了判定
if cmd == "end":
print("End!")
exit()
# ヘルプ
if cmd == "help":
usage()
continue
# 以降は、引数ありコマンドの処理
cmds = cmd.split()
if len(cmds) < 1:
usage()
continue
# 保存
if cmds[0] == "save":
if len(cmds) != 3:
usage()
continue
cmd_store[cmds[1]] = cmds[2]
# 取得
if cmds[0] == "get":
if len(cmds) != 2:
usage()
continue
print(cmd_store[cmds[1]])
# 削除
if cmds[0] == "remove":
if len(cmds) != 2:
usage()
continue
del cmd_store[cmds[1]]
# 一覧
if cmds[0] == "list":
print('"key","value"')
for k, v in cmd_store.items():
print(f'"{k}","{v}"')
1ファイルだから上に載せたのが全量だけど、一応、ソースは下記に置いている。
https://github.com/sky0621/book_py/tree/v0.1.0
解説
この規模なら、何かしらのプログラミング言語でコンソールアプリ作ったことあればやってることは理解できそうな気はする。
なので、部分部分だけピックアップ。
◆ヒアドキュメント
モダンな言語では、書かれたままを文字列として表現できるヒアドキュメントが使える。
残念ながらJavaには無いので、文字列の+結合で似た形に寄せる。
Ruby
def usage()
puts <<~EOB
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
〜〜〜
help ... ヘルプ情報(当内容と同じ)を表示します。
EOB
Golang
func usage() {
msg := `
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
〜〜〜
help ... ヘルプ情報(当内容と同じ)を表示します。
`
println(msg)
}
Java
private static void usage() {
String msg = "\n" +
"[usage]\n" +
"キーバリュー形式で文字列情報を管理するコマンドです。\n" +
〜〜〜
"help ... ヘルプ情報(当内容と同じ)を表示します。\n" +
"\n";
System.out.println(msg);
}
Scala
def usage(): Unit = println(
"""
|[usage]
|キーバリュー形式で文字列情報を管理するコマンドです。
〜〜〜
|help ... ヘルプ情報(当内容と同じ)を表示します。
|
|""".stripMargin)
Kotlin
fun usage() {
val msg = """
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
〜〜〜
help ... ヘルプ情報(当内容と同じ)を表示します。
""".trimIndent()
println(msg)
}
Python
msg = """
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
〜〜〜
help ... ヘルプ情報(当内容と同じ)を表示します。
"""
print(msg)
◆ハッシュマップ生成
Rubyは全てがオブジェクト。new
キーワードを使う。
ただ、Javaでは「new HashMap()
」となるが、Rubyでは「Hash.new
」となる。
そして型は明示化しなくてよい。(JavaもVer.10で型推論が導入されたけど)
対して、Golangではマップは特殊な構文。
Ruby
cmd_store = Hash.new
Golang
var cmdStore = map[string]string{}
Java
private static Map<String, String> cmdStore = new HashMap<String, String>();
Scala
import scala.collection.mutable
var cmdStore = mutable.Map.empty[String, String]
Kotlin
var cmdStore = mutableMapOf<String, String>()
Python
cmd_store = {}
◆無限ループ
処理待ち受けアプリでおなじみにの無限ループ。
他の言語だと「while true
」を使うことが多いけど、Rubyには(「while true
」も使えるけど)上記の書き方で実現できる。
Golangはとにかく言語自体をシンプルにしているので「for
」で無限ループも賄う。
Ruby
loop do
〜〜〜 ここの処理が(明示的に終了しない限り)無限ループ 〜〜〜
end
Golang
for {
〜〜〜 ここの処理が(明示的に終了しない限り)無限ループ 〜〜〜
}
Java
while (true) {
〜〜〜 ここの処理が(明示的に終了しない限り)無限ループ 〜〜〜
}
Scala
while (true) {
〜〜〜 ここの処理が(明示的に終了しない限り)無限ループ 〜〜〜
}
Kotlin
while (true) {
〜〜〜 ここの処理が(明示的に終了しない限り)無限ループ 〜〜〜
}
Python
while True:
〜〜〜 ここの処理が(明示的に終了しない限り)無限ループ 〜〜〜
◆標準入力
要件として、アプリ起動後は標準入力を待ち受ける状態となる。
Ruby
これを実現するのが「gets
」。これ書くだけで、この行でユーザからの入力を待ち受ける状態になる。
ちなみに、「chomp
」は標準入力から改行コードを除去するコード。
cmd, *args = gets.chomp.split /\s+/
Golang
OSからの標準入力をScanner
を用いて取得。
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
〜〜〜
cmds := strings.Split(s.Text(), " ")
〜〜〜
}
Java
JavaもScanner
を使う。
Scanner s = new Scanner(System.in);
String cmd = s.nextLine();
〜〜〜
// 以降は、引数ありコマンドの処理
String[] cmds = cmd.split(" ");
Scala
val cmds = io.StdIn.readLine().split(" ")
Kotlin
val s = Scanner(System.`in`)
val cmd = s.nextLine()
〜〜〜
// 以降は、引数ありコマンドの処理
val cmds = cmd.split(" ")
Python
cmd = input()
◆アプリ終了
Ruby
exit
Java
System.exit(-1);
Scala
sys.exit(-1)
Kotlin
exitProcess(-1)
Python
exit()
◆コレクション走査
言語によって特色の出やすいコレクション走査。
Rubyでは「each
」で各々の要素を操作する処理を明示し、「|k, v|
」という書き方で1件毎のキーとバリューを参照する。
Golangではコレクションはrange
で走査。
Javaでコレクションといったらstream
が登場。ただ、他の言語より書きっぷりが冗長?
Ruby
cmd_store.each {|k, v| puts %("#{k}","#{v}")}
Golang
for k, v := range cmdStore {
println("\"" + k + "\",\"" + v + "\"")
}
Java
cmdStore.entrySet().stream().map(e -> "\"" + e.getKey() + "\",\"" + e.getValue() + "\"").forEach(System.out::println);
Scala
for ((k, v) <- cmdStore) println("\"%s\",\"%s\"".format(k, v))
Kotlin
cmdStore.forEach { k, v -> println("\"$k\",\"$v\"") }
Python
for k, v in cmd_store.items():
print(f'"{k}","{v}"')
まとめ
簡単なツールレベル、かつ、言語特有の強みが出る書き方でなく、どれも似たような書きっぷりになるよう意識してしまった結果、構文としての多少の差異ぐらいしか見えなくなってしまった・・・。
言語の強みがちゃんと出るように書くようにしないと、お題を満たすものにはならないか・・・。
今回みたいな事例だとわかりづらいけど、例えばDBアクセスを伴う(いわゆる)Webシステムを(シビアなパフォーマンス要件はなく)早く作りたいとなると、Ruby
+ Ruby on Rails
の組み合わせは強いんじゃないかと思う。
(自分は仕事で一からこの組み合わせで作ったことはなく、あくまでチュートリアルレベルをやった上での感想)
Golang
は、(現場のエンジニアがGoしか使えないとかなければ)Webシステムを作るのに使おうと思った時の必然性には乏しいと個人的には思う。
逆に、マシンリソースを効率よく使ってパフォーマンスを上げたいといった時や、こじんまりとした(でもWindowsでもMacでもバイナリ1つで動く)開発補助ツールを作りたいといった時に、Golang
は適していると思う。
(自分は家でも仕事でも実際に上記のようなケースでGolang
を使った上での感想)
Java
並びにScala
、Kotlin
といったJVM系言語は、個人で何か小さくアプリ作成を始める場合には他により適した言語がありそうだけど、なんといっても、どんなアプリ、システムを作るにしてもそれなりに使いやすい汎用性の高さと、豊富な既存資産が活かせるといった強力な強みがあると思う。
(まあ、既存資産は捉え方によっては弱みとも言えるけど・・・)
以上、お題とも離れ、まとめにもならないまとめ。(余裕が出たら、次はWebシステム作成を通しての比較記事かなぁ。)