わみてっく

主に技術のことを記事にします。

KotlinにおけるDestructuring declarations(分割宣言)の小ネタ

Kotlinでプログラムを書く時、しばしば使う機能としてDestructuring declarations(分割宣言)*1があります。

例えば、以下のように、data classのプロパティをそれぞれ別の変数として宣言することで、独立した変数として取り扱うことができます。

data class Person(val name: String, val age: Int)

val masuo = Person("Masuo", 29)
val (name, age) = Person("Masuo", 29)
println("$name -san is now $age years old.") // Masuo -san is now 29 years old.

最近Kotlinでコードを書く中で、改めて分割宣言と触れる機会があったので、機能的な部分のおさらいをしつつ、使いどころについて考えてみます。

Destructuring declarationsとは

「Destructuring=構造を破壊する、分割」というその名の通り、値、オブジェクト、配列を複数の変数にdestructuring=分割して宣言することができるという機能です。他の言語ではJavaScriptPHP、Rustなどなど多くの言語で採用されています。

componentN() というもの

Kotlinで宣言された分割宣言は、分割した変数ごとに component1(), component2() といった、 componentN() (ここでのNは自然数をさす)関数としてコンパイルされます。なので、変数の数が増えていけば component3(), component4()......といった具合に数字が増えていきます。

この関数はdata class以外でも、HashMapやArrayといった多くの標準ライブラリのクラスで実装されているので、特に意識せず分割代入できるクラスには大体生えている、という理解で良いと思います。

ちなみに使う場面はないと思いますが、 componentN() 関数を自分で実装してしまえば、data classでないclassやobjectであっても分割宣言を利用することができます。

object HttpClient {
    private var host by Delegates.notNull<String>()
    private var port by Delegates.notNull<Int>()
    operator fun component1(): String {
        return this.host
    }
    operator fun component2(): Int {
        return this.port
    }
}

val (host, port) = HttpClient

分割宣言のメリット

分割宣言自体、とてつもなく便利!ないと困る!ような代物ではないと思います。ただLambdaで引数として受け取る値やData classについて、それ全体ではなく、その時々の関心事だけを、見通しよく、シンプルに表現できるのが良さだと思います。

処理を書く上で、値やオブジェクトの中の特定の部分にのみ処理をかけたい、あるいはその他の部分を無視したい、というときに覚えておくと、コードで表現する力が上がると思っています。

ユースケース

以下では実際の利用例を見ていきます。

Data classのパラメータ

Data classではデフォルトで componentN() が宣言されるため、パブリックなプロパティは全て分割宣言で取り出すことができます。また、特定のプロパティのみを取り出すことも可能です。

data class Person(val name: String, val age: Int)

val (name) = Person("Katsuo", 11)
println("Hey ${name.uppercase()}!!!") // Hey KATSUO!!!

Pair型

Pair型のプロパティは first, second の名前で取り出すことができますが、分割宣言で取り出すことも可能です。以下のコードでは、下2行の出力結果は同じになります。

val product = "Apple" to 200
val (name, price) = "Apple" to 200
println("${product.first} is ${product.second} yen.")
println("$name is $price yen.")

Lambdaのパラメータ

KotlinでCollectionを操作するときに登場する map()reduce() などでお馴染みかと思いますが、Lambdaの引数も分割宣言が可能です。

val persons = listOf(Person("Masuo", 29), Person("Katsuo", 11), Person("Wakame", 9))
persons.forEach { (name, age) -> println("After 10 years, ${name.uppercase()} will be ${age + 10} years old.") }

// After 10 years, MASUO will be 39 years old.
// After 10 years, KATSUO will be 21 years old.
// After 10 years, WAKAME will be 19 years old.

なおLambda式自体は、実際に引数がいくつに分割可能なのかを意識しません。そのため、以下のように想定しないエラーが発生するケースもあります。

val numbers = listOf(listOf(1,2,3), listOf(1,2))
numbers.forEach { (a,b,c) -> println("$a + $b = $c") }

// 1 + 2 = 3
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
// at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4266)

実用を考えると、そのままの値を用いるより個別のData classを作って名付けるようにするほうが良いでしょう。

アンダースコア

利用しない変数には _ をつけられます。関心を絞る意味でも、利用しないのであればアンダースコアをつけてわかりやすくしたほうが良いでしょう。

val products = mapOf("Apple" to 200, "Banana" to 300, "Orange" to 400)
products.forEach { (_, v) -> println("$v yen!!!") }

// 200 yen!!!
// 300 yen!!!
// 400 yen!!!

小ネタもいいところでしたが今日はここまで。


参考

https://kotlinlang.org/docs/destructuring-declarations.html#destructuring-in-lambdas

https://maku77.github.io/kotlin/basic/dest-decl.html

*1:Kotlinの日本語公式ドキュメントは見当たらなかったのですが、Android developersのこのページに語として登場していたので、それを用いています。

2023年の簡単なふりかえり

2023年を大きくいくつかのカテゴリに分けて、簡単にふりかえってみる。

仕事

  • 引き続きWebアプリケーションを開発するソフトウェアエンジニア
  • 今年はKotlinに多く触れた年だった
    • サーバー、バッチ両方書いた
    • k8s上で稼働するServiceとJobどっちも作った
  • 春頃からはNext.jsでフロントエンドも書いた
    • 後述の通り6月から育休に入ったので、そこまでNext.jsらしいことはできなかった
  • 1 on 1のコーチ、メンターとしての経験を積んだ
    • 以前に比べて、相手の言語化を助けたり、相手が暗黙的に感じていることを引き出したりする質問ができるようになったと思う
    • 実際のプロジェクト上の悩みを相談してもらって、次回以降に状況が進展しているのを聞くと嬉しかった
  • 5月に2週間ほど出張して、新しい開発チームの立ち上げを行った
  • 6月から育休に入ったので、実質半年くらいの稼働で終了

技術

自転車

  • 200kmブルベを一つ完走した
  • Mt.富士ヒルクライムでブロンズリングを獲得した
    • 約87分
    • 初めて参加してから3回目、ようやく達成できたので嬉しい
    • なお出走にあたっては家庭内での綿密な調整があったということを念の為記しておく
  • 子供が生まれてからはマイペースに週1くらいでライドに出て、あとはたまにZwiftでペーサーライドをしている

生活

  • 6月に第一子が生まれた
  • それに伴って育休を半年(当初)取得した
    • 育休を一定期間取ることに迷いは一切なかった
    • それを了承してくれた職場には感謝しかない

育休のふりかえり

  • 1,2ヶ月目
    • 思えばこの時期が一番しんどかった
      • 睡眠時間が少ない、育児何もわからない、実家が遠くて頼れる人がいない、でなかなかタフだった
      • さらに夫婦で新型コロナ感染症に罹患してしまったときはいよいよ終わった、と思った
    • 幸い子供の方は大病もなく、なんとか乗り切れた
  • 3,4ヶ月目
    • 睡眠時間も確保できて、生活が安定し始めた
    • 子供の成長に一喜一憂しつつ、自分の時間も取れ始めた
    • 子供はまだそこまで動けないので、今思うと一番ラクな時期だったかもしれない
      • そのかわり怒涛の保育園見学に行ってクタクタだった
  • 5,6ヶ月目
    • 離乳食が始まった
      • 今のところまだ苦戦している
    • 保育園の年度途中入園はかなわなかった
      • ありがたいことに3月末まで育休を延長できたので、ひたすら育児に向き合うことにする
    • 子供は順調に成長してきており、行動範囲が広がってきた

その他

  • 30歳になった
  • 6月中旬からWrite Code Every Dayを実践している
    • 今の所続いているが、帰省などもあっていつまで続けられるかは謎

山あり谷ありではあったが、今までで一番充実した一年だったと思う。

幸い新型コロナ感染症も軽症で済み、小さな体調不良はいくらかあったものの大病はしなかった。

来年は育児と仕事の両立が最大のテーマになる。

ソフトウェアエンジニアとして自分のエッジとなる技術をひたすら磨きたいし、自分の時間も大切にしたい。

来年はどんな一年になるのだろうか。楽しみである。

KotlinのjoinToString関数に関する小ネタ

今でもたまにやらかすので供養として。

Kotlinにはいくつか便利な関数がありますが、その中にjoinToStringがあります。

joinToString - Kotlin Programming Language

上に貼ったリンク先でも読めますが、Iterableをレシーバーとして、要素を連結して文字列に変換する関数です。


ただ、便利なあまりたまに紛らわしい挙動をすることがあります。

例えばこんなふうに。

fun main() {
    val list = listOf("a", "b", "c")
    // 1
    println(list.joinToString(","))
    // 2
    println(list.joinToString { "," })
    kotlin.system.exitProcess(0)
}

joinToString 関数を使った処理を2つ書いてみました。遠目に見ると同じ処理に見えなくもないです。

いやいや、全然違うじゃん、という人も、出力結果を予想しながら続きを読んでみてください。

fun main() {
    val list = listOf("a", "b", "c")
    // 1
    println(list.joinToString(",")) // a,b,c
    // 2
    println(list.joinToString { "," }) // ,, ,, ,
    kotlin.system.exitProcess(0)
}

全く違う結果になりました。不思議ですね。

これは、Kotlinのコーディング規約と、joinToStringの引数の合わせ技によって起こっています。ここから先は気になった人だけ読んでみてください。

Kotlinのコーディング規約

Kotlinのコーディング規約の中には以下のようなものがあります。

According to Kotlin convention, if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses: Higher-order functions and lambdas | Kotlin Documentation

つまり、関数の最後の引数が関数であった場合は、その部分だけラムダ式としてカッコの外側に書き出すことができる、ということなのです。

実際に以下のようなコードを見たことがある人もいるのではないでしょうか。

val product = items.fold(1) { acc, e -> acc * e }

上記はfold関数の最後の引数は、operation: (acc: R, T) -> Rという、各要素に対する操作を表す関数です。よって、関数部分に当たるラムダ式がカッコの外側に書き出されている、というわけです。

今回のjoinToString関数も、transform: ((T) -> CharSequence)? = null という関数になっており、{ "," }というラムダ式で、常に","が返ってくる関数になってしまっている状態というわけです。

ちなみに、以下のページにて、Kotlinの開発元である、Jetbrainsがコーディング規約をまとめています。気になった人は覗いてみてください。

Coding conventions | Kotlin Documentation

joinToStringの引数

上でも少し触れましたが、joinToStringの最後の引数は関数を受け取ります。

では、最初の引数は何か、というと、separator、つまり区切り文字を表します。

ちなみにこのseparatorには、デフォルト値として", "が与えられています。

つまり、以下のコードは「", "を区切り文字として」「要素を常に","に変化させる関数を適用する」ことによって生まれた出力ということになります。

    println(list.joinToString { "," }) // ,, ,, ,

おわりに

joinToStringがなんだかまぎらわしい関数のように思えてきた人もいるかもしれませんが、他にもprefix/postfixを指定できたり、いろいろな機能があります。

val numbers = listOf(1, 2, 3)
println(numbers.joinToString()) // 1, 2, 3
println(numbers.joinToString(prefix = "[", postfix = "]")) // [1, 2, 3]
println(numbers.joinToString(prefix = "<", postfix = ">", separator = "+")) // <1+2+3>

気になった人は以下のPlaygroundで実際に触って確かめてみてください。

Kotlin Playground: Edit, Run, Share Kotlin Code Online

Project Eulerを始めて1ヶ月が経ち50問解き終わった話

Project Eulerを始めましたという話を先日したのですが、

i-whammy.hatenablog.com

先日最初の50問を解き終わりました。

自分のリポジトリの最初のコミットの日付から、おおよそ1ヶ月前くらいから始めたことがわかります。

github.com

節目っちゃ節目なので、後でふりかえれるように記録として残しておきます。

雑感

何だかんだ毎日続いてる

現在育児休業中(4ヶ月目)で、最近だと保育園探しなどの生活の諸々が慌ただしくなってきました。育児も慣れてきたものの、それなりに時間は必要になります。

そんな生活の中でも、毎日1問は必ず解ききる、というのは習慣として続いています。

ここまでの問題はそこまで難しくはないものが多かったのですが、新しい習慣が定着しつつあるのは純粋に嬉しいですね。

分からない時の見切りが早くなった

ごく初期は、どんな問題も多少の時間をかけてでも自力で解ききることを大事にしていました。

とはいえ、生活の諸々が慌ただしくなったことと、他にもやりたいことはたくさんあるのとで、見切りをつけるタイミングを以前より早くすることにしました。

今では分からない問題は、以下のリポジトリを参考にさせていただいて自分の手で解ききるようにしています。

github.com

コードを見て、なんとなくこうだなぁ、で止めるよりも、手を動かして、問題と解法を体感することは大事だなぁと思います。

Kotlinの標準ライブラリのありがたみを感じる

問題を解いていく中で、頻出する処理が出てきます。具体的には、ループ処理、条件分岐、コレクション操作、ソートなど。これらの処理の大部分は、Kotlinに標準ライブラリとして実装されています。例えば、コレクション操作であれば、map、reduce、fold、filter。条件分岐は定番のif/else以外にも、when、takeIfなど。

ただ問題を解くだけではなく、Kotlinの標準ライブラリと機能(特に拡張関数)を使って、KotlinのいうExpressivenessを少しずつ体現できるようになってきたような気がします。

後はDSL等を使って、Kotlinの提供する機能をもう少しフルに使っていきたいですね。

0! = 1と知った

はい。

これから

続けられる限りは毎日解き続けて行こうと思います。問題の難易度はこれからどんどん上がっていくと思うので、今からとても楽しみです。

最近子の成長をひしひしと感じるので、負けないように、地道に地道にやっていきます。

Arrowでfunctionalなエラーハンドリングを始める

はじめに

ArrowはKotlinの関数型プログラミングのためのライブラリです。 ScalaHaskellといったプログラミング言語で見られる関数型プログラミングで見られるデータ型(Either等)や、操作といった抽象をKotlinに持ち込んで、もっと純粋な関数型プログラミングしようぜ!というモチベーションから生まれたようです。

arrow-kt.io github.com

この記事では、その中でも使う機会が多いであろうエラーハンドリングについて、Arrowが提供するEither型を例として取り上げてみます。

動作環境

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS"

$ java --version
openjdk 17.0.7 2023-04-18
OpenJDK Runtime Environment Temurin-17.0.7+7 (build 17.0.7+7)
OpenJDK 64-Bit Server VM Temurin-17.0.7+7 (build 17.0.7+7, mixed mode, sharing)

$ /opt/idea-IC-231.9011.34/bin/idea.sh --version
CompileCommand: exclude com/intellij/openapi/vfs/impl/FilePartNodeRoot.trieDescend bool exclude = true
IntelliJ IDEA 2023.2.2 (Community Edition)
Build #IC-232.9921.47

また、pom.xmlには以下の依存性を追加しています。

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>1.9.0</version>
        </dependency>
        <dependency>
            <groupId>io.arrow-kt</groupId>
            <artifactId>arrow-core</artifactId>
            <version>1.2.0</version>
        </dependency>
    </dependencies>

Kotlinにおけるエラーハンドリング

そもそも、Kotlinではどのような方法でエラーハンドリングが実現されているのでしょうか。

try-catch文

まず思いつくのが、Javaと同様に、try-catch文を書くパターンです。

data class Denominator(val value: Int)
data class Numerator(val value: Int)

fun Denominator.divideWithTryCatch(numerator: Numerator): Int {
    try {
        return this.value / numerator.value
    } catch (e: ArithmeticException) {
        // some error handling logic
        // e.g.) log("You cannot pass 0 as numerator!")
        throw e
    }
}

ラフではありますが、渡されたnumeratorで割り算をした結果を返すInt型の拡張関数を例として考えます。

この関数は、numeratorを0として呼び出された場合に正しく動作しません。数学上、0で割り算を行うことはできないからです。そのような場合を想定して、上のコードでは、計算時に例外が発生した際にthrowされるArithmeticExceptionをcatchして、ハンドリングを行っています。

このようなハンドリングが物足りないと感じる点を一つ挙げると、「取り扱うドメイン内での失敗にあたる場合」「プログラムを実行する上で発生した例外にあたる場合」もまとめて、Exception型を継承したクラスで表現しなければならない部分にあると思います。Arrow内では、前者をlogical failure(ロジック上の失敗)、後者を(real)exceptions(例外)として区別しています。

さらにそもそもの話にはなりますが、Javaの世界にある検査例外は、Kotlinには存在しません。そのため、関数のシグネチャ上からどのような例外がthrowされるか判断することができませんし、検査例外がハンドルされていなくても、Javaのようにコンパイルエラーで気付くことができません*1

runCatchingとResult型

KotlinにはrunCatchingという関数が用意されています。これは渡されたblock式を実行して、結果をKotlinのResult型として返すものです。

data class Denominator(val value: Int)
data class Numerator(val value: Int)

fun Denominator.divideWithRunCatching(numerator: Numerator): Int {
    return runCatching { this.value / numerator.value } // Result<Int>
        .fold(
            // on success
            { n -> n },
            // on failure
            { e ->
                // some error handling logic
                throw e
            }
        )
}

先程実装したものと同じような拡張関数を作ってみました。

KotlinのResult型にはfold関数が拡張関数として用意されています。これはonSuccess, onFailure2つの関数をブロックとして渡すことができ、前者は成功時にResultに対して型Tを引数に取って何らかの型Rを返し、後者はThrowableを引数に取り同様に型Rを返すものです。

従来のtry-catchと比べると、メソッドチェーンを作ったり、より関数型に近い表現を行うことができます。しかし、現在KotlinのResult型では、失敗時にはThrowableを継承した型のみしか利用できない上、Resultというシグネチャ上では、成功時の型のみしかわからず、失敗時にどのような例外が返されるかは依然としてわからないままです。

Arrowでエラーハンドリングしてみよう

サンプルを書き直してみる

ではArrowを使うとどのようなエラーハンドリングを実現できるのでしょうか。Arrowが提供しているEither<E,A>型を使って先程のサンプルを書き直してみます。

import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure

data class Denominator(val value: Int)
data class Numerator(val value: Int)

object DividedByZero

fun Denominator.divideWithArrow(numerator: Numerator): Either<DividedByZero, Int> = either {
    ensure(numerator.value != 0) { DividedByZero }
    value / numerator.value
}

eitherのブロックは2つの引数を取ります。1つは失敗時、もう1つは成功時の型を要求しています。また、ensureはArrowの関数で、Predicate(真偽値を返す関数)と、遅延するEitherの失敗時の型(ここではDividedByZero)を引数として取ります。つまりpredicateで示す条件に合致しないときは、失敗時の型を返します。

このように、Either型を利用して、簡単に成功時と失敗時の型を関数のシグネチャとして示せるようになりました。失敗時の型も任意の型で表現できるようになったので、ドメイン上の失敗と、プログラム上の例外も区別しやすくなっています。

返されたEither型をハンドルする際には、whenを使って網羅的にカバーすることができます。

fun example() {
    val answer = Denominator(10).divideWithArrow(Numerator(0))
    when (answer) {
        is Either.Left -> {
            // some handling logic here!
            answer.value // DividedByZero
        }
        is Either.Right -> {
            answer.value // Int
        }
    }
}

Eitherの実践

ここまでであれば、sealed interfaceを利用して自前で実装することもできるレベルかと思います。ここからは、Eitherを利用してもっと色々とやってみましょう。

catchOrThrowを使って例外をより具体的にする

新しくファイルが作られた後に、そのファイルパスの中身を読み取る処理を考えてみましょう。

ファイル読み取り時の失敗はIOException、そのサブクラスによって表現されますが、ファイルが存在しなかった場合のみは「ファイルが作られていない状態」として定義できるとしましょう。

object FileNotCreated
fun readCreatedFile(filePath: String): Either<FileNotCreated, String> {
    return Either.catchOrThrow<IOException, String> {
        File(filePath).readBytes().toString(Charsets.UTF_8)
    }.mapLeft { e ->
        if (e is FileNotFoundException) FileNotCreated
        else throw e
    }
}

上のコードでは、FileNotFoundExceptionが発生したときにはFileが作られていなかったことを型で表現し、それ以外の例外の場合にはそのままrethrowするような実装をすることができます。

バリデーションの重ねがけをしていく

ユーザーを登録する際に、複数のバリデーション(重複チェックや無効な文字列が含まれていない等)をかけるケースを考えてみましょう。

登録に失敗するような問題が発生したら、UserRegisterProblemを実装したクラスを、そうでなければ登録されたユーザーを返したいとします。

sealed interface UserRegisterProblem {
    data class MailAddressAlreadyExists(val mailAddress: String): UserRegisterProblem
    data class InvalidMailAddress(val invalidMailAddress: String): UserRegisterProblem
    data class InvalidUserIdCharacter(val invalidUserId: String): UserRegisterProblem
}

data class UserRegisterRequest(val id: String, val mailAddress: String)
data class RegisteredUser(val id: String, val mailAddress: String)

fun UserRegisterRequest.invoke(): Either<UserRegisterProblem, RegisteredUser> = either {
    ensure(mailAddress.exists()) { UserRegisterProblem.MailAddressAlreadyExists(mailAddress) }
    ensure(mailAddress.isInvalid()) { UserRegisterProblem.InvalidMailAddress(mailAddress) }
    ensure(id.containsInvalidCharacter()) { UserRegisterProblem.InvalidUserIdCharacter(id) }
    RegisteredUser(id, mailAddress)
}

上のように、eitherブロックでは、複数のensure句を受け付けることができます。これによって重ねがけができるようになりました。

recoverで例外時のハンドリング処理を書く

Either型に生えている拡張関数では、失敗時のハンドリング処理を書くことができます。これは、引数にEitherの失敗時の型を取る関数を渡すものです。

これによって、業務上失敗が発生しても後続のフォールバック処理を進めたい、という場合に、正しくかつ簡潔にフォールバック処理を記述することができます。また、内部で渡された失敗の型を、よりドメインに即した型に変換して返すことも可能です。

object MoreDomainDetailedError : Error
fun handleUsingRecovery(denominator: Denominator, numerator: Numerator): Either<Error, Int> {
    return denominator.divideWithArrow(numerator)
        .recover { _: Error -> raise(MoreDomainDetailedError) }
}

さいごに

Arrowはこの他にもLensesやConcurrencyなど、様々な機能を提供しています。ぜひ気になった機能を触ってみてください。

今回記事中に登場したソースコードは以下のリポジトリで公開しています。

github.com


References

Working with typed errors | Arrow

Exceptions | Kotlin Documentation

Kotlinにおける型の世界と エラーハンドリング / Type World and Error Handling in Kotlin - Speaker Deck

*1:Kotlinの公式ドキュメントでは、Bruce Eckelのコメントを引用しつつ、Javaのような他の言語からの呼び出し時には、@Throwsアノテーションをつけてthrowされる例外を明記するように記載しています。こうすることで、Kotlinのコード内で検査例外をthrowしても、Javaのコード上から問題なく呼び出せるようです。

Project Eulerと数学とわたし

この記事は

最近Project Eulerというものにハマっています。 projecteuler.net 簡単にいうと、プログラミングを使って数学の計算問題をたくさん解いていこうぜ、っていうサイトです。

この記事は、Project Eulerを始めたいちソフトウェアエンジニアの話です。

自己紹介

わたしは

わみ というソフトウェアエンジニアです。JavaやKotlin、後はTypeScriptとかClojureとか細々と書いてます。

最近子どもが生まれて、絶賛育休中です。寝かしつけって奥が深いですよね。

わたしと数学

高校数学を数学ⅡBまで修了しています。いわゆる文系学部卒だと思ってもらえるとよいかと思います。

どちらかといえば得意な方でしたが、高校を卒業して10年以上が経過しているのでもはやあまり関係ないんじゃないかしら、という気持ちです。

Project Eulerとは

2001年から長く続く、プログラミングによって計算問題を解くサイトです。例として、以下のような問題が英語で出題されているので、答えとなる数字を入力して解答するという非常にシンプルなつくりになっています。

If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3,5,6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000.

(意訳:10未満の自然数のうち、3または5の倍数は3,5,6,9である。これらの合計は23である。1000以下の自然数について、3または5の倍数の和を求めよ。)

世界中に何十万人以上の利用者がおり、各問題に正答者のみが閲覧できるフォーラムが用意されています。そこではそれぞれの正答者がたどり着いた解法が紹介されていたりします。2023年9月末現在で846問が掲載されており、最近でも不定期で問題が追加されているようです。

Wikipediaにも記事があるので、詳しく知りたい方はそちらもご覧ください。 ja.wikipedia.org

わたしとProject Euler

きっかけ

とある同僚に「お前の問題の解き方はなっていない(意訳)」と言われて紹介を受けたのがきっかけです。

一応ソフトウェアエンジニアとして10年弱のキャリアがあるのですが、アルゴリズムと真剣に向き合うことなくここまで過ごしてきました。

そんな訳なので、アルゴリズムを実践の中で学びつつ、いわゆる「プログラマ的な問題の解き方」が身についたらいいなーくらいの気持ちで始めました。

どのくらい解いたか

始めて10日前後、ざっくり30題解きました。解くときに書いたコードは以下のリポジトリになんとなく上げてます。 github.com

特にノルマや目標は決めていないのですが、それでも1日最低1題は解くようにしています。単体テストは書いたり書かなかったりしています。複雑な問題だなと感じたら、テスト駆動で少しずつ解きほぐしながら解いてる感じです。

どうやって解いてるか

プログラミング言語はKotlin、IDEIntelliJ IDEAを使っています。問題ごとにファイルを分けて、それぞれにmain関数を生やして、答えやそれに近い状態のものをprintln()で出力させてます。

Kotlinで書いてるのは、自分の中で一番経験がある言語なので問題を解くことにある程度集中できるかなと思ったのが理由です。

とはいえ、中には数字を文字列的に捉えて解くような問題もあるので(各桁の数字の積を求めるなど)、その時は若干めんどくささを感じています。

Project Eulerを始めてよかったこと

色々な解き方を発見できた

ここまで解いた問題は、「一定の範囲内の数値について、とある条件に当てはまるものを発見する」性質のものが多かったです。

プログラムでいうと、制御構造(if文など)やループ(forなど)を駆使して解くことになるわけですね。Java育ちの私は最初手続き的な解き方を多くしてきました。例えば以下みたいな感じですね。

// 1000以下の自然数のうち、3もしくは5で割り切れる数字の和を求める
fun hoge() {
    var answer = 0
    for (i in 1..999) {
        if (i % 3 == 0 || i % 5 == 0) answer += i
    }
    println(answer)
}

Project Eulerでは、特に実行時間や実行環境など、解法に対する厳しい縛りは用意されていません。(普通のPCで、常識的な範囲内で解けるくらいのアルゴリズムにしてね、くらい)なので、どのような解き方をするかは基本的に自由なわけです。

ところが、この方法で問題を解いていくうちに、「せっかくKotlinを使ってるし、もっと違った解き方をしてみたいな」という気持ちになってきました。KotlinはJava由来の手続き型な記法だけでなく、関数型のエッセンスも取り入れた言語となっています。なので最近では、より関数型に近い形で解くことを目指しています。上と同じ問題であれば、以下のように書けますね。

// 1000以下の自然数のうち、3もしくは5で割り切れる数字の和を求める
fun functionalHoge() {
    val answer = (1..999).filter { it % 3 == 0 || it % 5 == 0 }.reduce { acc, i -> acc + i }
    println(answer)
}

関数型や手続き型を行ったり来たりしつつ、より良い解き方を模索していくのは楽しいですね。

錆びついた数学の記憶を掘り起こせた

高校を卒業して10年以上が経過しているので、高校数学の記憶はさすがに薄れてきました。とはいえ、日常業務の中で数学的知識が要求される場面は少なくありません。そんな中で、Project Eulerの問題を正しく解くためには、数学の知識が必要不可欠になるわけです。

問題を解いていく中で、「一回習ったけど忘れていた知識たち」と再会を果たすのはなかなか面白いなと思っています。具体的には、順列と組み合わせ、素数無理数あたりが挙げられるでしょうか。

さいごに

Project Eulerは楽しいのでみんな始めてみましょう。

個人的にはProject Euler仲間がいるとモチベーションにつながるので、もしやってるよ〜っていう人がいたらX(旧Twitter)で教えてください。

「Linuxで動かしながら学ぶTCP/IPネットワーク入門」を読了した

まえがき

私はWebエンジニアである。名前はあるが、腕に自信はない。

とはいえWebエンジニアとして生きていると、日々ネットワークに触れて生活をすることになる。

ネットワークについて断片的な知識しかなかったので、体系的な知識を得よう!と思って手に取ったのがこの「Linuxで動かしながら学ぶTCP/IPネットワーク入門」である。

ざっくりした内容

本書ではLinuxのNetwork Namespaceという機能を使って、仮想的にホストマシンのネットワークから独立したコンピュータを作ったりしながら、各レイヤーのプロトコルを体感することができる。

TCP/IPの説明から始まり、イーサネットトランスポート層、アプリケーション層*1、最終的にソケットプログラミングまで手を動かして学ぶことで、具体的なイメージをつかみやすくなっている。

読んだ感想

この本は、全体を通して概念的な説明と、具体的なハンズオンをバランスよく提供してくれている、と思った。

冒頭で触れたように、私はネットワークについて断片的な知識しか持てていなかったが、「今までなんとなく知ってたこいつは、こういうことだったのか!!」と点が線になる感覚があって楽しかった。

何より、実際に動くものを作れるのはすごく楽しい!Namespaceを作って、簡単なネットワークインターフェースを構築して、と一つ一つ段階を踏んでいって、最終的にNamespace間でpingが疎通するようになった時は嬉しかった。

内容としては基本情報/応用情報技術者試験の勉強のときに触れたはずだが、あの時は何となく頭の上を通り過ぎる感覚があったものが、もう少ししっかりと輪郭を捉えられたような気がする。

ボリュームも200ページ強とちょうどよく、サクサク読み進めることができた。

おまけ

私は「技術書を買うときは物理本で!」と決めているので、ハードカバー版を購入した。

ただどうやらKindle Unlimitedでは無料で読むことができるらしい。あとUnlimitedじゃなくても1200円とハードカバー版と比べると割安で入手できるようだ。

*1:OSI参照モデルではなく、TCP/IPにおける通信の階層構造の最上位レイヤー。ただし本書では、現場での共通理解を作る観点から主にOSI参照モデルの言葉を用いている。 参照:https://www.rfc-editor.org/rfc/rfc1122