木勉強メモ

木構造の取扱はプログラマなら基本なのかもだけれど、アルゴリズムとデータ構造をちゃんと勉強したことなかったので、必要ベースで勉強していく。

用語

用語説明
根(root)親を持たないノード。1つの木に対して1つ根。
辺(edge)親から子へのリンク。
葉(leaf)子を持たないノード。
兄弟(siblings)親が同じ子たち。
祖先(ancestor)p → q への経路がある場合、pをqの祖先と呼ぶ。
子孫(descendant)p → q への経路がある場合、qをpの子孫と呼ぶ。
レベル(level)ある木の深さの全ノードの集合を木のレベルと呼ぶ。
(ノードの)深さ(depth)根からノードまでの経路の長さ。
(ノードの)高さ(height)ノードから、一番深いノードまでの経路の長さ
木の高さ(Height of the tree)
木の深さ(depth of the tree)
木の全ノードの最大の高さ。
ノードのサイズ自分自身を含めた子孫の数
木に関する用語

二分木

どのノードも、子を、1または2しか持たない時、二分木(binary tree)と呼ぶ。

二分木の構造はデータフィールドの脇に左右の子を指すリンクを持たせる形。参考資料に合わせてCだと下記のようになる。

struct BinaryTreeNode {
    int data;
    struct BinaryTreeNode *left;
    struct BinaryTreeNode *right;
};

Scalaの場合は、leftright プロパティとして自身と同じ型の値を持つと考えて。

case class BinaryTreeNode(
    data: Int,
    left: Option[BinaryTreeNode],
    right: Option[BinaryTreeNode]
)

二分木の演算

二分木には基本演算と補助的演算が存在する

基本演算

  • 木に要素を挿入
  • 木から要素を削除
  • 要素を探索
  • 木を横断

補助的演算

  • 木のサイズを求める
  • 木の高さを求める
  • 最大和をもつレベルを求める
  • あるノード対の最小共通祖先(LCA)を求める。など。

二分木を横断する

前順序横断

  1. 根を訪問
  2. 前順序で左部分木を横断
  3. 前順序で右部分木を横断

Cでの実装(Cちゃんと書いたことないから変かもしれない)

#include <stdio.h>

struct BinaryTreeNode {
    int data;
    struct BinaryTreeNode *left;
    struct BinaryTreeNode *right;
};

void PreOrder(struct BinaryTreeNode *root) {
    if (root) {
        printf("%d", root->data);
        PreOrder(root->left);
        PreOrder(root->right);
    }
}

struct BinaryTreeNode makeBinaryTreeNode(int data, struct BinaryTreeNode *left, struct BinaryTreeNode *right) {
    struct BinaryTreeNode temp;
    temp.data = data;
    temp.left = left;
    temp.right = right;
    return temp;
}

int main() {

    struct BinaryTreeNode node7 = makeBinaryTreeNode(7, NULL, NULL);
    struct BinaryTreeNode node6 = makeBinaryTreeNode(6, NULL, NULL);
    struct BinaryTreeNode node5 = makeBinaryTreeNode(5, NULL, NULL);
    struct BinaryTreeNode node4 = makeBinaryTreeNode(4, NULL, NULL);
    struct BinaryTreeNode node3 = makeBinaryTreeNode(3, &node6, &node7);
    struct BinaryTreeNode node2 = makeBinaryTreeNode(2, &node4, &node5);
    struct BinaryTreeNode node1 = makeBinaryTreeNode(1, &node2, &node3);

    PreOrder(&node1);

    return 0;
}

// 実行結果
// 1245367</pre>

Scalaで同じことをする。

def preOrder(root: BinaryTreeNode): Unit = {
  print(root.data)
  root.left.foreach(preOrder)
  root.right.foreach(preOrder)
}

val node7 = BinaryTreeNode(7, None, None)
val node6 = BinaryTreeNode(6, None, None)
val node5 = BinaryTreeNode(5, None, None)
val node4 = BinaryTreeNode(4, None, None)
val node3 = BinaryTreeNode(3, Some(node6), Some(node7))
val node2 = BinaryTreeNode(2, Some(node4), Some(node5))
val node1 = BinaryTreeNode(1, Some(node2), Some(node3))

preOrder(node1) // 1245367

もう少し、Scalaらしく書いて見る。Scalaで二分木を表現するならば、sealed trait を用いて

sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]</pre>

のように表現できる。trait Tree のメソッドとして preOrder を定義すると

sealed trait Tree[+A] {
  def preOrder: Unit = this match {
    case Leaf(value) =&gt; print(value)
    case Branch(value, left, right) =&gt;
      print(value)
      left.preOrder
      right.preOrder
  }
}

val leaf7 = Leaf(7)
val leaf6 = Leaf(6)
val leaf5 = Leaf(5)
val leaf4 = Leaf(4)
val branch3 = Branch(3, leaf6, leaf7)
val branch2 = Branch(2, leaf4, leaf5)
val branch1 = Branch(1, branch2, branch3)

branch1.preOrder // 1245367
branch2.preOrder // 245
branch3.preOrder // 367</pre>

全要素を走査できるならば、map が定義できそう。

sealed trait Tree[+A] {
  ...

  def map[B](f: A =&gt; B): Tree[B] = this match {
    case Leaf(value) =&gt; Leaf(f(value))
    case Branch(value, left, right) =&gt; Branch(f(value), left.map(f), right.map(f))
  }
}

branch1.map(v =&gt; v * 10).preOrder // 10204050306070</pre>

foreach を定義してあげれば、preOrder 不要。

sealed trait Tree[+A] {
  ...

  def foreach[U](f: A =&gt; U): Unit = {
    this match {
      case Leaf(value) =&gt; f(value)
      case Branch(value, left, right) =&gt;
        f(value)
        left.foreach(f)
        right.foreach(f)
    }
  }
}

二分木で要素を探索する。C言語版。要素が存在したら、1を返し、無ければ0を返す。

int FindInBinaryTreeUsingRecursion(struct BinaryTreeNode *root, int data) {
    int temp;
    if (root == NULL) return 0;
    else {
        if (data == root-&gt;data)
            return 1;
        else {
            temp = FindInBinaryTreeUsingRecursion(root-&gt;left, data);
            if (temp != 0) return temp;
            else return (FindInBinaryTreeUsingRecursion(root-&gt;right, data));
        }
    }
}

これを参考にScala版。引数に A を引数にとって Boolean を返す関数をとり、Option[A] を返す。左から順に調べる。

def find(f: A =&gt; Boolean): Option[A] = {
    this match {
      case Leaf(value) =&gt; if (f(value)) Some(value) else None
      case Branch(value, left, right) =&gt;
        if (f(value)) Some(value)
        else {
          val temp = left.find(f)
          if (temp.isDefined) temp else right.find(f)
        }
    }
  }

参考

Haskell 勉強メモ8

高階関数

Haskell は関数を引数にとったり、返り値として関数を返すことができる。このような関数を高階関数と呼ぶ。Scalaにもある。

カリー化関数

Haskellの全ての関数は、引数を1つだけ取る。今まで、複数引数を取っているように見えたものは全てカリー化された関数だ。

Prelude> :t max
max :: Ord a => a -> a -> a

Prelude> max 4 5
5

max 関数は2つの引数を取り、大きい方を返す関数のように見えるが実際はカリー化された関数。まず、max 4 が適用され、max 4 は1つの値を引数にとる別の関数を返す。返ってきた関数に対して、5が適用されるという流れだ。

Prelude> let max' = max 4
Prelude> :t max'
max' :: (Ord a, Num a) => a -> a
Prelude> 

Scalaもカリー化できる。

def max(a: Int)(b: Int): Int = if (a >= b) a else b

max(4)(5) val res0: Int = 5
val max2 = max(4)(_) // val max2: Int => Int = <function>

Scalaの場合はHaskellほど素直ではなく、適用しない部分には _ を入れる必要があるようだ。また、関数部分適用はカリー化されていなくてもできて

def multiThree(x: Int, y: Int, z: Int): Int = x * y * z

val m2 = multiThree(3, _, _) // val m2: (Int, Int) => Int = <function>
m2(2, 4) // val res2: Int = 24

のように書くこともできる。

今度は、Int を引数にとって100と比較する関数を作る。これは単純に書くとこうだが

compareWithHundred :: Int -> Ordering
compareWithHundred x = compare 100 x

*Main> compareWithHundred 99
GT

実際には下記のように書くことができる

compareWithHundred :: Int -> Ordering
compareWithHundred = compare 100

compare 100 が引数を一つとって、Ordering を返す関数であるからだ。

Apache POI 調査

Excelを操作したいみたいな状況は割とよくある。Pythonとかのスクリプト言語だと結構対処法をが見つかるけど、Scalaだと、Java資産のApache POIくらいしかない上に情報もそんなにない(poi.scala のようなラッパーライブラリもあるようだけど、プロダクションで使うには早計感ある)。Apache POIの使い方を調べて、人並みに使えるようになろうと思う。参考記載のJavaでのApache POIの使い方を元に、Scalaで書いて感じを掴む。

準備

必要なライブラリを build.sbt に追記する。

libraryDependencies ++= Seq(
      "org.apache.poi" % "poi"       % "5.0.0",
      "org.apache.poi" % "poi-ooxml" % "5.0.0"
)

Excelファイルの作成

build.sbt の準備ができたら、Excelファイルを作成していく。

XLS ファイルの作成

今だと使う機会は少ないかもしれないが一応。XLSファイルを扱うのは、上記のライブラリのうち poi の方だ。

package dev.tchiba.researchApachePoi

import org.apache.poi.hssf.usermodel.HSSFWorkbook

import java.io.FileOutputStream
import scala.util.{Failure, Success, Try}

object XLS {
  def main(args: Array[String]): Unit = {
    val fileName = "demo.xls"
    val workbook = new HSSFWorkbook() // XLSにはHSSFWorkbookを使う
    workbook.createSheet("Tab1")
    Try(new FileOutputStream(fileName)) match {
      case Failure(exception) => exception.printStackTrace()
      case Success(outputStream) =>
        workbook.write(outputStream)
        println(s"An Excel file $fileName has been created")
        workbook.close()
    }
  }
}

XLSX ファイルの作成

XLSXファイルを作成するには、build.sbt 記載のライブラリのうち、後者の poi-ooxml が必要だ。処理はほとんど同じで、生成するインスタンスのクラスのみが異なる。

package dev.tchiba.researchApachePoi

import org.apache.poi.xssf.usermodel.XSSFWorkbook

import java.io.FileOutputStream
import scala.util.{Failure, Success, Try}

object XLSX {
  def main(args: Array[String]): Unit = {
    val fileName = "demo.xlsx"
    val workbook = new XSSFWorkbook() // XLSX を生成する場合は、XSSFWorkbookを使う
    workbook.createSheet("Tab1")
    Try(new FileOutputStream(fileName)) match {
      case Failure(exception) => exception.printStackTrace()
      case Success(outputStream) =>
        workbook.write(outputStream)
        println(s"An Excel file $fileName has been created")
        workbook.close()
    }
  }
}

Excelファイルにデータを書き込む

空のExcelファイルが作れても一ミリも嬉しくないので、データを書き込んでいこう。サンプルに下記のようなデータモデルを用意する。

case class Person(
    firstName: String,
    lastName: String,
    birthDay: LocalDate,
    email: String,
    phoneNumber: String,
    married: Boolean
)

これをExcelに書き込むクラスを用意しよう。参考がJavaなので、あんまり Scala っぽくないけど、下手に外れると詰まるから諦める。

package dev.tchiba.researchApachePoi

import models.Person

import org.apache.poi.ss.usermodel.{Sheet, Workbook}
import org.apache.poi.xssf.usermodel.{XSSFSheet, XSSFWorkbook}

import java.io.FileOutputStream
import scala.util.{Failure, Success, Try}

class PersonExcelWriter {

  def write(fileName: String, personList: List[Person]): Unit = {
    val workbook = prepareWorkbook(personList)
    Try(new FileOutputStream(fileName)) match {
      case Failure(exception) => exception.printStackTrace()
      case Success(outputStream) =>
        workbook.write(outputStream)
        println(s"An Excel file $fileName has been created")
        workbook.close()
    }
  }

  private def prepareWorkbook(personList: List[Person]): Workbook = {
    val workbook: XSSFWorkbook = new XSSFWorkbook()
    val personSheet: XSSFSheet = workbook.createSheet("Persons")
    prepareHeader(personSheet)
    prepareTable(personSheet, personList)
    workbook
  }

  private def prepareHeader(personSheet: Sheet): Unit = {
    // ヘッダー行を作成する
    val headerRow = personSheet.createRow(0)

    // セルを作成する
    val firstNameHeaderCell = headerRow.createCell(0)
    val lastNameHeaderCell  = headerRow.createCell(1)
    val birthdayHeaderCell  = headerRow.createCell(2)
    val emailHeaderCell     = headerRow.createCell(3)
    val phoneNumberCell     = headerRow.createCell(4)
    val marriedHeaderCell   = headerRow.createCell(5)

    // セルに値をセットする
    firstNameHeaderCell.setCellValue("First name")
    lastNameHeaderCell.setCellValue("Last name")
    birthdayHeaderCell.setCellValue("Birthday")
    emailHeaderCell.setCellValue("Email")
    phoneNumberCell.setCellValue("Phone number")
    marriedHeaderCell.setCellValue("Married")
  }

  private def prepareTable(personSheet: Sheet, personList: List[Person]): Unit = {
    personList.zipWithIndex.foreach { case (person, index) =>
      // 行を作成する
      val row = personSheet.createRow(index + 1)

      // セルを作成する
      val firstNameCell   = row.createCell(0)
      val lastNameCell    = row.createCell(1)
      val birthdayCell    = row.createCell(2)
      val emailCell       = row.createCell(3)
      val phoneNumberCell = row.createCell(4)
      val marriedCell     = row.createCell(5)

      // セルに値をセットする
      firstNameCell.setCellValue(person.firstName)
      lastNameCell.setCellValue(person.lastName)
      birthdayCell.setCellValue(person.birthDay.toString)
      emailCell.setCellValue(person.email)
      phoneNumberCell.setCellValue(person.phoneNumber)
      marriedCell.setCellValue(person.married)
    }
  }
}

手順としては

  1. ワークブックを用意する
  2. シートを用意する
  3. カラムにデータを埋めていく
  4. 最後に、ワークブックをファイルに保存する

となる。

これを使う処理を書こう。

package dev.tchiba.researchApachePoi

import models.Person

import java.time.LocalDate

object Main {
  def main(args: Array[String]): Unit = {
    val person1 = Person(
      firstName = "太郎",
      lastName = "田中",
      birthDay = LocalDate.of(1990, 2, 2),
      email = "taro.tanaka@xxx.com",
      phoneNumber = "090XXXXXXX",
      married = true
    )

    val person2 = Person(
      firstName = "花子",
      lastName = "佐藤",
      birthDay = LocalDate.of(1992, 9, 9),
      email = "hanako.sato@xxx.com",
      phoneNumber = "090YYYYYYY",
      married = false
    )

    val personList = List(person1, person2)

    val writer = new PersonExcelWriter()
    writer.write("person.xlsx", personList)
  }
}

実行して出力されたファイルを開いてみると、(私のPCにはExcel入っていないので、Numbersを利用)、想定通りに値が入っていることが分かる。

参考

Apache POI に関する体系的な情報が探しづらいなか、下記の本がKindle Unlimited にあったのが救いだった。

Haskell勉強メモ 7

クイックソート

アルゴリズムの定番クイックソート

といっても、私はアルゴリズムをちゃんと勉強したことないので名前しか知らないのだけれど。。

これを関数型ではどうやって解くのかを見ていく。

が、先に手続き型ではどうやるのか。アルゴリズム本を見ながら慣れてるScalaで手続き的に書いてみる。

クイックソートとは

配列を分割して、それぞれ再帰的に解いてまとめる分割統治法にのっとったアルゴリズム

適当にpivotを選び出し、配列全体を「pivot未満のグループ」と「pivot以上のグループ」に分割してそれぞれ再帰的に解く。

Scalaで手続き的に書く場合(参考

def quickSort(xs: Array[Int]): Array[Int] = {
  def swap(i: Int, j: Int): Unit = {
    val t = xs(i)
    xs(i) = xs(j)
    xs(j) = t
  }
  def sort(left: Int, right: Int): Unit = {
    val pivot = xs((left + right) / 2)
    var i     = left
    var j     = right

    while (i <= j) {
      while (xs(i) < pivot) i += 1
      while (xs(j) > pivot) j -= 1
      if (i <= j) {
        swap(i, j)
        i += 1
        j -= 1
      }
    }
    if (left < j) sort(left, j)
    if (j < right) sort(i, right)
  }
  sort(0, xs.length - 1)
  xs
}

写経してみたが、手続き的な実装はいまいち分からん。。

Haskell

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x : xs) =
  let smallerOrEqual = [a | a <- xs, a <= x]
      larger = [a | a <- xs, a > x]
   in quicksort smallerOrEqual ++ [x] ++ quicksort larger

*Main> quicksort [10,8,1,3,4,12,4,1,3,9,8]
[1,1,3,3,4,4,8,8,9,10,12]

なんと分かりやすいことか。。ピボット a より小さいものと大きいものに分けてそれを再帰する。とても分かりやすい。

これをScalaで書いてみる。

def quickSort(list: List[Int]): List[Int] = {
  list match {
    case Nil => Nil
    case ::(x, xs) =>
      val smallerOrEqual = for (a <- xs; if a <= x) yield a
      val larger = for (a <- xs; if a > x) yield a
      quickSort(smallerOrEqual) ++ List(x) ++ quickSort(larger)
  }
}

ロジックがそのまま記述されており、手続き的な実装と比べて明らかに読みやすい。

Haskell 勉強メモ6

再帰

関数型言語では再帰はよく出てくる。

最大値を求める関数を考えてみる。命令的に考えると、現在の最大値を保持する変数を用意し、リストの全ての要素についてループして、変数を更新していく形になる。Scalaで書いてみると

def maximum(list: List[Int]): Int = {
  var max = list.head
  list.foreach { v =>
    if (v > max) max = v
  }
  max
}

となる。再帰的に書く場合最初に基底部(解を明示的に定義しなければならないケース)を定義しなければならない。単一要素のリストに対する最大値は、その唯一の要素と同じ値である。それ以上の場合は、リストの先頭要素と残りのリストの最大値でどちらが大きいかを見る。Scalaで書いてみる。

def maximum(list: List[Int]): Int = {
  def max(a: Int, b: Int): Int = if (a >= b) a else b
  list match {
    case Nil => throw new IllegalArgumentException
    case ::(head, next) if list.size == 1 => head
    case ::(head, next) => max(head, maximum(next))
  }
}

要素が1つのみの表現がわからなかったので、ガードでかいた。

Haskellでは

maximum' :: (Ord a) => [a] -> a
maximum' [] = error "maximun of empty list!"
maximum' [x] = x
maximum' (x : xs) = max x (maximum' xs)

*Main> maximum' [1, 3, 5, 0, 13, 11, 12]
13

replicate

Int と値をとり、その値を指定された数だけ繰り返したリストを返す。

Haskell には元々ある。

*Main> replicate 3 5
[5,5,5]

基底部を考える。0 以下の回数だけ複製することはできないので、この際は空のリストを返す。

replicate' :: Int -> a -> [a]
replicate' n x
  | n <= 0 = []
  | otherwise = x : replicate' (n - 1) x

Scalaで書く場合

def replicate[T](n: Int)(x: T): List[T] =
  n match {
    case k if k <= 0 => Nil
    case k           => x +: replicate(n - 1)(x)
  }

replicate(3)(5) // val res2: List[Int] = List(5, 5, 5)

Scala側はカリー化する必要はあまりないが、Haskell と合わせている。

take

takeは指定されたリストから指定された数を返す。

take' :: Int -> [a] -> [a]
take' n _
  | n <= 0 = []
take' _ [] = []
take' n (x : xs) = x : take' (n -1) xs

*Main> take' 3 [1,2,3,4,5]
[1,2,3]

Scalaでの記述

def take[T](n: Int)(xs: List[T]): List[T] =
  (n, xs) match {
    case (k, _) if k <= 0    => Nil
    case (_, Nil)            => Nil
    case (k, ::(head, next)) => head +: take(k-1)(next)
  }

take(2)(List(1,2,3,4,5)) // val res3: List[Int] = List(1, 2)

Scalaのリストメソッドには、takeはあらかじめ実装されているので実用上はわざわざ作る必要はない。

override def take(n: Int): List[A] = if (isEmpty || n <= 0) Nil else {
    val h = new ::(head, Nil)
    var t = h
    var rest = tail
    var i = 1
    while ({if (rest.isEmpty) return this; i < n}) {
      i += 1
      val nx = new ::(rest.head, Nil)
      t.next = nx
      t = nx
      rest = rest.tail
    }
    releaseFence()
    h
  }

手続き的な実装にはなっているが。

reverse

関数をリストに取り、逆順のリストを返す

reverse' :: [a] -> [a]
reverse' [] = []
reverse' (x : xs) = reverse' xs ++ [x]

Scala だと

def reverse[T](xs: List[T]): List[T] = xs match {
  case Nil => Nil
  case ::(head, next) => reverse(next) :+ head
}

reverse(List(1,2,3,4,5)) // val res3: List[Int] = List(5, 4, 3, 2, 1)

repeat

受け取った要素から無限リストを作る関数。Haskellは基本遅延評価だから、無限リストでも途中で切るようにとれば問題ない。

*Main> take 3 (repeat 5)
[5,5,5]

再帰的実装は

repeat' :: a -> [a]
repeat' x = x : repeat' x

*Main> take 3 (repeat' 3)
[3,3,3]

Scalaの通常のListは遅延評価ではないから、真似はできないか調べたら LazyList っていうのがあるのを知った。

val lazyList: LazyList[Int] = LazyList.continually(1).take(15)
var count = 0
lazyList.foreach { v => count += v}
println(count) // 15

API ドキュメントに記載されているサンプルが分かりやすい。

val fibs: LazyList[Int] = {
  0 #:: 1 #:: fibs.zip(fibs.tail).map { n =>
    println(s"Adding ${n._1} and ${n._2}")
    n._1 + n._2
  }
}
fibs.take(5).foreach(println)
// 0
// 1
// Adding 0 and 1
// 1
// Adding 1 and 1
// 2
// Adding 1 and 2
// 3

fibs.take(6).foreach(println)
// 0
// 1
// 1
// 2
// 3
// Adding 2 and 3
// 5

アクセスされて初めて評価されていることが分かる。

zip

2つのリストを引数にとり、これらを綴じ合わせて返す。

Prelude> zip [1,2] ['a', 'b', 'c']
[(1,'a'),(2,'b')]

Scala

List(1,2,3).zip(List("a", "b")) // val res9: List[(Int, String)] = List((1,a), (2,b))

実装。どちらか一方が空のリストの時は、空のリストを返す。

どちらの先頭要素もあれば、先頭要素をとったタプルを作り、再帰する

zip' :: [a] -> [b] -> [(a, b)]
zip' _ [] = []
zip' [] _ = []
zip' (x:xs) (y:ys) = (x, y) : zip' xs ys

Scalaで書くと

def zip[A, B](a: List[A])(b: List[B]): List[(A, B)] =
  (a, b) match {
    case (_, Nil)               => Nil
    case (Nil, _)               => Nil
    case (::(x, xs), ::(y, ys)) => (x, y) +: zip(xs)(ys)
  }

zip(List(1, 2, 3))(List("a", "b")) // val res11: List[(Int, String)] = List((1,a), (2,b))

elem

値とリストを受け取り、その値がリストに含まれるか調べる関数

*Main> 1 `elem` [2,3,4]
False

Scalaだと contains にあたるかなと。

List(2,3,4).contains(1)

Haskell 再帰での実装。基底部は空のリストを受け取った時のFalse。リストに要素がある場合は、先頭と値を比較して一致しなければ、残りの要素に対して再帰する。

elem' :: (Eq a) => a -> [a] -> Bool
elem' a [] = False
elem' a (x:xs)
    | a == x = True
    | otherwise = a `elem'` xs

Scalaでの実装

@tailrec
def elem[A](a: A)(list: List[A]): Boolean = (a, list) match {
  case (_, Nil) => false
  case (a, ::(x, _)) if a == x => true
  case (a, ::(_, xs)) => elem(a)(xs)
}

elem(1)(List(2,3,4)) // val res13: Boolean = false
elem(1)(List(2,1,3)) // val res14: Boolean = true

Haskell 勉強メモ5

let式

let 式は変数を束縛する。 let 式自身も式。式であるとは、「値を持つ」ということ。

円柱の表面積を求める。

cylinder :: Double -> Double -> Double
cylinder r h =
  let sideArea = 2 * pi * r * h
      topArea = pi * r ^ 2
   in sideArea + 2 * topArea

*Main> cylinder 4 10
351.85837720205683

let は式であるので、コードの中のどんなところでも使える。

*Main> 4 * (let a = 9 in a + 1) + 2
42

-- ローカルスコープに関数を作る
*Main> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]

-- タプル要素を分解してそれぞれの名前に束縛する
*Main> (let (a, b, c) = (1, 2, 3) in a + b + c) * 100
600

case 式

パターンマッチで使う。

head' :: [a] -> a
head' xs = case xs of
  [] -> error "No head for empty lists"
  (x : _) -> x

*Main> head' []
*** Exception: No head for empty lists
CallStack (from HasCallStack):
  error, called at baby.hs:3:9 in main:Main
*Main> head' [1, 2]
1
*Main> 

case 式の構文は下記

 case expression of pattern -> result
                pattern -> result
                pattern -> result

式に合致した最初のパターンが使われる。

case 式はどこでも使える。

describeList :: [a] -> String
describeList ls =
  "The list is "
    ++ case ls of
      [] -> "empty"
      [x] -> "a singleton list"
      xs -> "a longer list"

*Main> describeList []
"The list is empty"
*Main> describeList [1, 2]
"The list is a longer list"
*Main> describeList [1]
"The list is a singleton list"

Haskell 勉強メモ4

関数を書くための構文

パターンマッチ

Scala でもたくさん使うパターンマッチ。Haskell版も見てく。

渡された数が7かどうかを判別する関数。

Haskell

lucky :: Int -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"

*Main> lucky 8
"Sorry, you're out of luck, pal!"
*Main> lucky 7
"LUCKY NUMBER SEVEN!"

lucky を呼ぶと、パターンが上から下の順で試される。

Scala

def lucky(n: Int): String = n match {
  case 7 => "LUCKY NUMBER SEVEN!"
  case _ => "Sorry, you're out of luck, pal!"
}

lucky(8) // val res11: String = Sorry, you're out of luck, pal!
lucky(7) // val res12: String = LUCKY NUMBER SEVEN!

階乗計算するプログラムをパターンマッチを使って、再帰的に書く。

factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)

Scalaの場合

def factorial(n: Int): Int = n match {
  case 0 => 1
  case k => k * factorial(k-1)
}

タプルのパターンマッチもできる。

2次元ベクトルの足し算。

addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors a b = (fst a + fst b, snd a + snd b) -- これだと読みづらい。
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2) -- こう書くと直感的

リスト内包表記でもパターンマッチが使える。

*Main> let xs = [(1,3),(4,3),(2,4),(5,3),(5,6),(3,1)]
*Main> [a + b | (a, b) <- xs]
[4,7,6,8,11,4]

-- マッチしない要素は結果に反映されない。
*Main> [x*100 + 3 | (x, 3) <- xs]
[103,403,503]
val list = Seq((1,3),(4,3),(2,4),(5,3),(5,6),(3,1))
for ((a, b) <- list) yield a + b // val res13: Seq[Int] = List(4, 7, 6, 8, 11, 4)
for ((x, 3) <- list) yield x*100 + 3 // val res14: Seq[Int] = List(103, 403, 503)

リストもパターンマッチで使える。空のリスト [] または、: を含むパターンと合致させることができる。x:xs というパターンは、リストの先頭要素を x に束縛し、リストの残りを xs に束縛する。

head' :: [a] -> a 
head' [] = error "Can't call head on an empty list, dummy!"
head' (x: _) = x

*Main> head' []
*** Exception: Can't call head on an empty list, dummy!
CallStack (from HasCallStack):
  error, called at baby.hs:40:12 in main:Main
*Main> head' [2,3,4]
2

Scalaのパターンマッチの場合は下記。

def head[T](list: List[T]): T = list match {
  case x::_ => x
  case Nil => throw new IllegalArgumentException
}

head(List(2,3,4)) // val res0: Int = 2
head(List()) // java.lang.IllegalArgumentException

asパターン

Haskellには as パターン という特殊なパターンがある。値をパターンに分解しつつ、パターンマッチの対象になった値自体も参照したいときに使う。@ を使うことで、元のリスト全体にアクセスすることができる。

firstLetter :: String -> String
firstLetter "" = "Empty string whoops!"
firstLetter all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

*Main> firstLetter "Dracula"
"The first letter of Dracula is D"

知らなかったが Scala でも同じことができるらしい。参考

上と似たことをScalaでやると。

def firstLetter(letters: List[Char]): String = {
  letters match {
    case Nil => "Empty string whoops!"
    case all @ (x::xs) => s"The first letter of ${all} is $x"
  }
}

firstLetter("Dracula".toList) // val res3: String = The first letter of List(D, r, a, c, u, l, a) is D

@ を使うことで、元の要素にアクセスできていることが分かる。Scalaの場合は、元の要素をそのまま使えばいいんじゃとか思うけど、何か違うのだろうか。。?

ガード

引数の値が満たす性質で場合分けするとき。

bmiTell :: Double -> String
bmiTell bmi
  | bmi <= 18.5 = "underweight."
  | bmi <= 25.0 = "normal"
  | bmi <= 30.0 = "fat"
  | otherwise = "Are you Whale?"

参考本のサンプルはもっと毒々しかったけど、書くの面倒だった。bmi の値で場合分けしている。

Scalaで書く場合は

def bmiTell(bmi: Double): String =
  bmi match {
    case v if v <= 18.5 => "underweight"
    case v if v <= 25.0 => "normal"
    case v if v <= 30.0 => "fat"
    case v              => "Are you whale?"
  }

複数引数でも可能。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | weight / height ^ 2 <= 18.5 = "underweight."
  | weight / height ^ 2 <= 25.0 = "normal"
  | weight / height ^ 2 <= 30.0 = "fat"
  | otherwise = "Are you Whale?"

where

命令型のプログラミング言語なら、同じ値を何度を計算するのを避けるため、変数に格納するやることが一般的。Haskellでは、where キーワードをつかて計算の中間結果に名前をつける。さっきの bmiTell だと where /height ^ 2 を何度も書く&再評価するのは無駄が多い。下記のように修正する。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | bmi <= 18.5 = "underweight."
  | bmi <= 25.0 = "normal"
  | bmi <= 30.0 = "fat"
  | otherwise = "Are you Whale?"
  where
    bmi = weight / height ^ 2

where のなかでパターンマッチを使うこともできる。

initials :: String -> String -> String
initials firstname lastname = [f] ++ " " ++ [l] ++ "."
  where
    (f : _) = firstname
    (l : _) = lastname

where ブロック内では、定数だけでなく関数も定義できる。

calcBmis :: [(Double, Double)] -> [Double]
calcBmis xs = [bmi w h | (w, h) <- xs]
  where
    bmi weight height = weight / height ^ 2