Magnolia Tech

いつもコードのことばかり考えている人のために。

2018年版・この処理Scalaでどう書く?…前半戦

ふと、この記事にあるようなこと、Scalaだったらどう書くかな?と思ってまとめてみました。途中で力尽きたので、まずは前半戦まで!

www.m3tech.blog

あの処理、Scalaでどう書く?

基本的にScalaの標準ライブラリでやる方法を紹介し、Javaの標準ライブラリを使う必要があるところはその旨書いてあります。

また、調べていくとScalaJavaの標準ライブラリではできないことも有ったので、そこには「標準ライブラリではできません」と書いておきます。標準ライブラリ以外でやれる方法が有ったら、ぜひ教えてください。

なお、標準ライブラリはScala 2.12、Java 11の機能に基づいています。

標準出力・標準エラー出力

どちらもScalascala.Consoleオブジェクトのprintlnメソッドを使います(パッケージ名のscalaは省略可なので、以降は省略します)。

println("HELLO") // 標準出力にメッセージ

Console.err.println("ERROR!") // 標準エラー出力にメッセージ

単にprintlnと書いた場合は、Console.out.printlnが呼び出されます。

なお、標準ライブラリにはログ出力ライブラリは用意されていません。

ファイル関係

基本的にScalaの標準ライブラリでは、ファイル関係のサポートがほぼ無いので、基本的にJavaの標準ライブラリを使います。

パスの操作

Java 7から追加されたjava.nio.file.Pathsクラスを使います。

import java.nio.file.Paths

val p = Paths.get("/foo/bar/baz.txt") // OSごとの実装を持つPathインタフェースが返される
p.getFileName // ファイル名「baz.txt」を保持するPathが返る
p.getParent // 親ディレクトリ「/foo/bar/」を保持するPathが返る

val dirpath = Paths.get("/foo/bar")
dirpath.resolve("baz.txt") // パスの連結 「/foo/bar/baz.txt」を保持するPath…引数は文字列でもPathでもOK

Pathは、toStringで文字列に戻せます。また、Java6以前に作られたライブラリはjava.io.Fileクラスを要求するものがありますが、toFilejava.io.Fileに変換できます。

import java.nio.file.Paths

val fullpath = Paths.get("/foo/bar/baz.txt")
fullpath.toString // 文字列として「/foo/bar/baz.txt」が返る
fullpath.toFile // java.io.Fileでラップされた「/foo/bar/baz.txt」が返る

チルダ環境変数が含まれるパスを扱う

ScalaJavaチルダ環境変数をパスに展開してくれる標準ライブラリは無いようです。

実現しているライブラリがあればぜひ教えてください。

ファイルの読み書き

テキストデータの読み込みにはScalascala.io.SourceオブジェクトのfromFileメソッドが便利です。

val source = io.Source.fromFile("foo.txt", "utf-8")
val lines = source.lines
lines.foreach(println) // 行単位で標準出力へ
source.close

fromFileメソッドは文字列でのパス名以外にもjava.io.Fileクラスのオブジェクトも指定可能です。また、scala.io.Sourceはデフォルトで文字コードとして"UTF-8"が使われるようになっていますが、明示的に指定した方が分かりやすいでしょう。

反対に書き込みについては、専用のライブラリは(なぜか)用意されていないので、Java 7で導入されたFiles.newBufferedReaderメソッドを使います。ただし、Scalaにはtry-with-resource構文が無いので自前でクローズします。

import java.nio.file.Paths
import java.nio.file.Files
import java.nio.charset.StandardCharsets

val path = Paths.get("foo.txt")
val writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)

try {
  writer.append("内容")
  writer.newLine
} finally {
  writer.close
} 

構造が簡単なうちは自前でcloseメソッドを呼び出すようなコードを書いていても大丈夫だと思いますが、コードの構造が大規模になってくるとcloseの呼び出しも複雑化してきます。このような場合、Javaであればtry-with-resource構文がありますし、C#にはusing構文があります。Scalaでも構文は用意されていませんが、コーディングテクニックとしての「ローンパターン(loan pattern)」と呼ばれるものがあります。ここでは説明は割愛しますが、調べてみると色々な書き方が有るようなので、試してみましょう。

行数を数える(wc -l)

特に専用の構文やライブラリが用意されているわけではないですが、先ほどのio.Sourceを使うとシンプルに書けます。

val source = io.Source.fromFile("foo.txt")
source.getLines.length

ファイルの列挙

Java7で導入されたFiles.newDirectoryStreamを使います。

import java.nio.file.Paths
import java.nio.file.Files

val files = Files.newDirectoryStream(Paths.get("."), "*.txt")

try {
  files.forEach(println)
} finally {
  files.close
}

ファイルの情報(存在確認・作成日時)

java.nio.file.Filesのメソッド群を使います。

import java.nio.file.Paths
import java.nio.file.Files

val f = Paths.get("foo.txt")
Files.exists(f) // 存在確認
Files.isRegularFile(f) // ファイル?
Files.isDirectory(f) // ディレクトリ?
Files.getLastModifiedTime(f) // 更新日時…戻り値の型がjava.nio.file.attribute.FileTimeであることに注意!

その他、ファイルシステム固有の属性(作成日時、アクセス日時)は、Files.getAttributeメソッドを使います。戻り値がobject型になってしまうので、キャストが必要な点が要注意です。

val createTime = Files.getAttribute(Paths.get("foo.txt"), "unix:creationTime").asInstanceOf[java.nio.file.attribute.FileTime]
val accessTime = Files.getAttribute(Paths.get("foo.txt"), "unix:lastAccessTime").asInstanceOf[java.nio.file.attribute.FileTime]

コピー・移動・削除

こちらもjava.nio.file.Filesのメソッド群を使います。

import java.nio.file.Paths
import java.nio.file.Files

Files.copy(Paths.get("foo.txt"), Paths.get("bar.txt")) // コピー
Files.delete(Paths.get("bar.txt")) // 削除
Files.move(Paths.get("foo.txt"), Paths.get("bar.txt")) // 移動

コピーや、移動はオプションとしてStandardCopyOptionが用意する定数を指定することができます。例えば、StandardCopyOption.REPLACE_EXISTINGを指定すると、すでにファイルが有っても上書きします。

import java.nio.file.Paths
import java.nio.file.Files
import java.nio.file.StandardCopyOption

Files.copy(Paths.get("foo.txt"), Paths.get("bar.txt")) // コピー
// foo.txtを書き換える処理
Files.copy(Paths.get("foo.txt"), Paths.get("bar.txt"), StandardCopyOption.REPLACE_EXISTING) // 2回目のコピーで上書き

と、ここまでで力尽きたので、続きは明日以降!

外部コマンド

単純に実行する

外部のコマンドを実行し、標準出力を受け取る:

環境変数やカレントディレクトリを変更する

リダイレクトを使う

パイプを使う

spawn → wait (外部コマンドを起動し、終了を待つ)

シェルを実行する【危険!!】

時刻関係

文字列関係

文字列への式埋め込み

ヒアドキュメント

コマンドライン引数

終了時の処理&シグナルをtrapする

HTTPリクエスト(curlwget の代替)

吉祥寺.pmへの参加者を募集しています

このブログは企業テックブログという訳でもないので、特にエンジニア募集とかないですけど、定期的に吉祥寺.pmというイベントをやっているので、良かったら参加してみてください。pmとはついていますが、Scalaトークも歓迎です!

kichijojipm.connpass.com

あと、良かったら、Twitterのアカウントもフォローしてください。設計のこととかツイートしています。

twitter.com