SE情報技術研究会’s blog

http://se-info-tech.connpass.com

2016-01-30 『JUnit実践入門』読書会(第8章)の振り返り

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

第8章 パラメータ化テスト

  • パラメータ化テストとは入力値と期待値をパラメータ化しテストする手法
  • ユニットテストではテストケースが多くなっても、テストケースごとの差異は入力値と期待値のみとなる場合が多い(入力値による正常系や異常系など)
  • 入力値と期待値をパラメータ化することでテストの見通しや入力値と期待値の追加や削除が容易になる
  • Theroiesテストランナーを使用することでパラメータ化テストが行える

テストデータの選択

  • 開発リソースも有限であるため、可能な限り少ないテストデータで効率良くテストすることが求められる
  • テストデータの選択方法はテストの品質に大きく影響を与える

どのくらいのテストデータが必要か?

同値クラスによるテストデータの選択

同値クラスとはテスト対象メソッドで同じ結果を返す入力値の集合

例:18歳以上かを判定するメソッドには次の2つの同値クラスがある

  • 18以上の値から1つ
  • 18未満の値から1つ

組み合わせによるテストデータの選択

  • 入力値が複数の値の組み合わせになる場合、組み合わせを考慮しないといけない
  • 全組み合わせを試すことは不可能なことがある

下記の基準より

「プログラムに混入する不具合のほとんどは単項目に関する不具合である」

テストデータの選択

  • すべての条件を満たす(成功)パターン
  • 入力値の各項目で対象の項目だけ失敗するパターン

どのテストデータを選択するか?

  • 同値クラスで分析しグループ分けする
  • 不具合は境界値付近で発生しやすいので境界値付近のデータを使用する (特に「以下」や「未満」など境界値を含むか含まないか)

コラム:妥当な値の範囲を制限する

  • 年齢など値に特化したオブジェクトを作成し入力可能な値を制限することでテストも効率的になる
  • プログラムのシンプルさとのトレードオフ

入力値と期待値のパラメータ化

テストしないといけないデータの組み合わせが多い場合、テストコードが長くなりすぎる

Theoriesテストランナーを使ったパラメータ化テストが有効

テストデータとテストメソッドを分割できる

Theories

  • パラメータ化テストをサポートするテストランナー
  • Enclosedテストランナーとの併用も可能
  • @Theoryでパラメータ化テストであることを宣言し、任意の引数(@DataPoint@DataPoints)が受け取れるようになる
  • @DataPointでフィールドをパラメータとして宣言する(1フィールド1パラメータの宣言しかできない)
  • @DataPointsでフィールドを複数のパラメータとして宣言する
@RunWith(Theories.class)
public class ParameterizedTest {
  
  @DataPoint
  public static int PARAM_1 = 1;
  @DataPoint
  public static int PARAM_2 = 2;
  
  public ParameterizedTest() {
    System.out.println("コンストラクタ");
  }
  
  @Theory
  public void 引数を受け取るテストケースTest(int param) {
    System.out.println("param=" + param);
  }
}

※ 本のサンプルを若干修正

実行結果

コンストラクタ
param=1
コンストラクタ
param=2

例:Enclosedテストランナーとの併用

@RunWith(Enclosed.class)
public class テストデータの構造化Test

  @RunWith(Theories.class)
  public static class パラメータ化Test1 {
    …略
  }

  @RunWith(Theories.class)
  public static class パラメータ化Test2 {
    …略
  }
  …略

複数のテストメソッドがある場合

  • @DataPointで宣言した型と@Theoryで宣言したテストメソッドの引数の型が同じものがパラメータ化テストの対象となる
  • このようなケースは直観的ではないのでEnclosedテストランナーで分割したほうがよい

複数の引数が定義されている場合

  • @DataPointで宣言されたパラメータのすべての組みあわせが実行される
  • 型が同じ場合は重複も含め全パターン実行される
  • そのような場合は引数と結果を持つフィクスチャオブジェクトを自作し、それをパラメータとして使う
  • フィクスチャオブジェクトはJUnitの機能ではなく自作のオブジェクト
  • 複数の引数が定義されているテストケースの場合、全組み合わせが実行されるので、戻り値が引数によって変わるものはテストできない

@DataPoints 複数のパラメータを定義するアノテーション

  • 1つのフィールドにフィクスチャオブジェクトなどの配列を定義し1レコードづつテストするためのパラメータ
  • テストデータと結果を外部リソースとして定義しやすくなる

組み合わせテスト

  • 複数の引数が定義されているテストケースで対象のパラメータ値の場合のみテストケースとして評価したい場合、assumeThatを使う(全組み合わせが実行されても問題無いようにする)

Assumeによるパラメータのフィルタリング

  • assumeThatメソッド(やassumeTrueメソッド)は検証結果が失敗でもテスト結果の失敗とせず、そのテストケースを終了する
  • assumeThatメソッドは検証失敗時にAssumptionViolateExceptionを発生させテスト結果を成功として処理する
  • import static org.junit.Assume.*;を宣言する
  • assumeThatメソッド@Theoryだけでなく@Testでも使える

assumeメソッドを使った例:

@DataPoints
public static final int[] VALUES = {1, 2, 3, 4, 5};

@Theory
public void 偶数のみ検証されるテスト(int value){
  assumeTrue(value %2 == 0);
  assertThat(value %2 == 0, is(true));
}

パラメータ化テストの問題

  • データの網羅性はJUnitが保証するわけではない(テスト開発者が自分で行う)
  • 失敗時の情報にパラメータの情報が少ない

※ 本には具体的な値がでないとあったがJUnit 4.12だと失敗時のパラメータの値の情報は出てるもよう

@Theory
public void 偶数は失敗するテスト(int value) {
  assertThat(value % 2 == 1, is(true));
}

実行結果

org.junit.experimental.theories.internal.ParameterizedAssertionError: 偶数は失敗するテスト("2" <from VALUES[1]>)
...略
Caused by: java.lang.AssertionError: 
Expected: is <true>
     but: was <false>
    ...略

テストコードのテストは必要か問題

2016-01-24の勉強会の雑談で出てきた話。

se-info-tech.connpass.com

テストコード上で複雑なことする場合、そのコードに対してテストが必要なのでは?

  • そもそもテストの結果とプロダクトコードの結果がどちらかが間違っている場合に一致する可能性低いので不要では?
    → ただし、入力値が0やnullなどはどちらかが間違ってても一致する可能性は高い
    → 入力パターンが少ない場合はどちらかが間違ってても一致する可能性は高い

  • 何をやっても必ず成功を返すようなテストケースが入る可能性があるのでテストは必要では?
    → TDDだと最初に全て失敗するようにしてから必ず成功するようなテストケースはつぶせるはず
    → すでにあるテストコードをリファクタして汎用性を高めたりする場合は、全て失敗するパターンは使えないのでは?
    → あえて一時的にプロダクトコードをおかしくして影響範囲をみるとか?
    → それを実際にやるか?

2016-01-24 『JUnit実践入門』読書会(第7章)の振り返り

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

第7章 テストフィクスチャ

テストフィクスチャとは?

  • テストフィクスチャとはテストで扱うデータや実行環境やオブジェクトの状態などのこと
  • テストフィクスチャの設定は工夫をしないとコードが長く複雑になる

ユニットテストのフィクスチャ

  • テスト対象オブジェクト
  • 入力値
  • 期待値
  • テスト実行前までに必要なテストオブジェクトの操作
  • 外部リソース
  • DBなどの外部システム
  • 依存クラスや依存外部システムのモックオブジェクト

フレッシュフィクスチャ

フレッシュフィクスチャとは下記のことを行う戦略

  1. フィクスチャはテストケースごとに取得する
  2. フィクスチャはテストケースの実行ごとに初期化する
  3. フィクスチャはテストケースの終了時に開放する

フィクスチャがいくつかのテストケースで共有されていると、実行順序や並列にテストを行うと結果が変わる可能性が高い

JUnitではテストの実行時にテストケースごとにテストクラスのインスタンスを生成している
→ テストクラスのインスタンス変数やローカル変数は各テストケースで共有されることがない

フィクスチャとスローテスト問題

  • フレッシュフィクスチャだとDBを扱う場合などに長時間になってしまう(スローテスト問題)
  • 解決策としてテストケースの並列実行、共有フィクスチャ、カテゴリ化テスト(第10章で説明)が有効
  • スローテスト問題はユニットテストの実行時間が何十分もかかるようになってから対応すべき

共有フィクスチャ

  • 共有フィクスチャとは各テストで使用するフィクスチャを共有し、再利用すること
  • セットアップコストが抑えられる

共有フィクスチャの問題

  • テストケースごとの独立性が弱くなる
  • グローバル変数の場合と同じ理由で、テストコードのメンテナンス性が下がる
  • 適切に後処理を行う必要がある

共有フィクスチャを使う場合

  • 不変オブジェクトにする

フィクスチャのセットアップパターン

インラインセットアップ

  • インラインセットアップとはテストケースごとにフィクスチャのセットアップを行うこと
  • セットアップがテストケースで完結できる
  • セットアップが複雑な場合、可読性が悪くなる

暗黙的セットアップ

  • 暗黙的セットアップとは@Beforeがついたセットアップメソッドにフィクスチャのセットアップを行い、各テストメソッドの実行前に暗黙的にセットアップを実行すること
  • テストケースはテスト実行と検証のコードがメインになり、可読性が高くなる
  • Enclosedランナーを使った場合に高い効果を期待できる

生成メソッドでのセットアップ

  • 独立したクラスにフィクスチャの生成メソッドを抽出し利用させるセットアップ方法
  • 複数のテストクラスでの共通のフィクスチャの生成が行えるようになる
  • staticメソッドで定義することでstaticインポートが使用でき、可読性が高くなる
  • 生成メソッド名を日本語で定義するとわかりやすくなる

外部リソースからのセットアップ

  • 外部に定義ファイルにテストデータを記述し、生成メソッドで読み込む手法
  • XMLは複雑なデータを記述できるが、メンテナンスが大変なため推奨しない
  • JSON複数行テキストの扱いが難しい
  • フィクスチャがリスト構造で大量の場合はCSVExcelが便利
  • 著者のおすすめはYAML
  • YAMLなどの外部リソースはテストケースとテストデータがそれぞれ独立したファイルになるため相互参照がしにくい

Java宣言的記法

  • Javaで宣言的な記述を行うには、匿名クラスを使って行うことができる
  • ただして匿名クラスの場合、final宣言されていると継承できないので使えない
  • Groovyを導入するでJavaの宣言的なコードをわかりやすく記述することが可能
    → 最近だとScalaでも可能かも?
    Scalaだとコンパイル遅い問題が出てきそう
  • テスト用のライブラリにGroovyを追加するのでプロダクトには影響しない
  • Groovyは学習コストも低く可読性も高い

2016-01-23 第13回 関数型プログラミング勉強会(Scala)の振り返り part.1

第6章 純粋関数型の状態

前回の続きより

純粋関数型の命令型プログラミング

本では下記の定義がある

val ns: Rand[List[Int]] =
  int.flatMap(x =>
    int.flatMap(y =>
      ints(x).map(xs => 
        xs.map(_%y))
    )
  )

これをダウンロードしたサンプルで実装しようとするとコンパイルエラーになる。

まず、ここでのRandはtype Rand[+A] = RNG => (A, RNG)ではなくStateで定義されたRandである。

ただし、ダウンロードしたサンプルのStateにはintやintsはない。

動かすためには下記を行う。

  • このns: Rand[List[Int]]のRandはobject State内で定義されているRandを使う。
  • intsはサンプルには定義されてないがExercise 6.7で定義されているものを使う。
  • intは定義されていないので自分で定義する必要がある。 ※ State内で定義すると楽です。
object State {
  type Rand[A] = State[RNG, A]

  …略

  val ns: Rand[List[Int]] = {
    int.flatMap(x => 
      int.flatMap(y =>
        ints(x).map(xs => xs.map(_ % y))))
  }

  // 
  def int: Rand[Int] = State(rnd => {
    rnd.nextInt
  })

  // Exercise 6.7より
  def ints(count: Int): Rand[List[Int]] = {
    sequence(List.fill(count)(int))
  }
}

ちなみに本でポイントにしたいのは下記

  • 命令型プログラミングを行うにはfor内包表記を使うとコードがわかりやすい

2016-01-16 第12回 関数型プログラミング勉強会(Scala)の振り返り

第6章 純粋関数型の状態

前回の続きより

状態の処理に適したAPI

  • RNG => (A, RNG) のように RNGの状態から別のRNGの状態を返すような関数を「状態アクション」または「状態遷移」と呼ばれる。
  • コンビネータ = ここでは状態アクションを行うための高階関数

Randの定義:

type Rand[+A] = RNG => (A, RNG)

Exercise 6.5

GitHubにあがってる解答で問題なし。 mapを使ったものとそうで無いものの実装を比較してみる

mapを使ってる場合

val _double: Rand[Double] =
  map(nonNegativeInt)(_ / (Int.MaxValue.toDouble + 1))

mapを使ってない場合

def double: Rand[Double] = rng => {
  val (i, r) = nonNegativeInt(rng)
  (i / (Int.MaxValue.toDouble + 1), r)
}

mapではnonNegativeIntが返すタプルの最初の要素に割り算を行った結果をもつタプルのリストを作っている

Exercise 6.6

GitHubにあがってる解答で問題なし。

解答のコメントには呼び出し元のRNGを第1引数のRandに渡し、そこで生成されたRNGを第2引数のRandに渡すように書いてある。 そうしないと第1引数にも第2引数にも同じRNGが渡され、両方で生成される結果が同じになるため期待した値にはならなない。

解答より:

def map2[A,B,C](ra: Rand[A], rb: Rand[B])(f: (A, B) => C): Rand[C] =
  rng => {
    val (a, r1) = ra(rng)
    val (b, r2) = rb(r1)
    (f(a, b), r2)
  }

なぜ2つのRandの引数部分と関数の引数部分が分かれているかの疑問があがった
→ 2つのRandを受け取ることと関数で処理をすることとでは性質が違うためかと思われる。

ここでカリー化の話も出てきた

def map2[A,B,C](ra: Rand[A], rb: Rand[B])(f: (A, B) => C)

def map2[A,B,C](ra: Rand[A])(rb: Rand[B])(f: (A, B) => C)

def map2[A,B,C](ra: Rand[A], rb: Rand[B], f: (A, B) => C)

は同じ

Exercise 6.7

GitHubにあがってる解答で問題なし。

パターンマッチを使うパターンで解答している参加者もいた。

def sequence0[A](fs: List[Rand[A]]): Rand[List(A)] = {
  rng: RNG => {
    fs match {
      case Nil => (Nil, rng) // unit(Nil:List[A])
      case h :: t => {
        val (hd, nextRng) = h(rng)
        val (tl, nextRng2) = sequence0(t)(nextRng)
        (hd :: tl, nextRng2)
      }
    }
  }
 }

::の話題も出たのでリストのおさらい。

  • 要素 :: リストで最初に要素を持ち、後ろにリストを持つ新しいリストができる
  • リストの最後はNil(空のList)
map2(r, acc)(_ :: _)

map2(r, acc)( (r1, acc1) => r1::acc1)

と同じ

入れ子の状態アクション

Exercise 6.8

GitHubにあがってる解答で問題なし。

Exercise 6.9

GitHubにあがってる解答で問題なし。

状態アクションデータ型の一般化

  • RandのメソッドはRNGに特定されているが汎用的にすることも可能
  • Rand型より汎用的なState型を作成
type Rand[+A] = RNG => (A, RNG)

type State[S, +A] = S => (A, S)

こうすることでRandをStateを使って表すことも可能

Exercise 6.10

解答ではsequenceをfoldRightと再帰とfoldLeftを使ったパターンを示している。

再帰の例のコメント

This implementation uses a loop internally and is the same recursion pattern as a left fold. It is quite common with left folds to build up a list in reverse order, then reverse it at the end. (We could also use a collection.mutable.ListBuffer internally.)

この実装はループを内部で使い、左のfoldと同じ 再帰パターンにしている。 左のfoldを逆の順にしてリストを構築し最後に 再度逆順にするのはよくあることだ。 (内部でListBufferを使うことも可能)

foldLeftの例のコメント

We can also write the loop using a left fold. This is tail recursive like the previous solution, but it reverses the list before folding it instead of after. You might think that this is slower than the foldRight solution since it walks over the list twice, but it's actually faster! The foldRight solution technically has to also walk the list twice, since it has to unravel the call stack, not being tail recursive. And the call stack will be as tall as the list is long.

左のfoldを使ったループで書くことも可能。 これは前の解決の末尾再帰のようなものだが foldする後ではなく前にリストを逆順にしている。 リストを2度たどるのでfoldRightより遅いと 思うかもしれないが、実際は早い。 foldRightもまた末尾再帰ではないので コールスタックをほどく必要があるので 技術的に2回リストをたどっている。 そしてリストが長いほどコールスタックも高く積まれる。

2016-01-11 『ふつうのHaskellプログラミング』 もくもく読書会(第12章) の振り返り

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門

  • 作者: 青木峰郎,山下伸夫
  • 出版社/メーカー: SBクリエイティブ
  • 発売日: 2014/10/05
  • メディア: オンデマンド (ペーパーバック)
  • この商品を含むブログを見る

第12章 Wikiエンジンの開発

今回はまずはサンプルプログラムをコンパイルして動かしてみる。

参照:『ふつうのHaskellプログラミング』サポートページ――LazyLinesの入手とインストール
http://i.loveruby.net/ja/stdhaskell/lazylines.html

  • 本のサンプルは古いHaskellで実装されているので、サンプルコードはそのままでは動かない。
  • WebサーバなどのプログラムはRubyで書かれているがダウンロードファイルにサーバの実行ファイルが用意されているのでRubyのインストールなどは不要

Haskellの古いバージョンから新しいのに変える修正はこちらを参考にしました。

Haskellを始めるにあたって読むべき3冊の本 - セカイノカタチ::Techlog

ここにGitHubに修正したものを置いてあるリンクがあるので、そこのソースと比較して修正すると動きました(GHC 7.10.2)

github.com

修正箇所

  • 現在のHaskellだとPreludeのcatchがなくなっているのでSystem.IO.ErrorのcatchIOErrorに変更する必要がある
  • System.IO.Errorがインポートされていないソースファイルでcatchを使っている個所はimport System.IO.Errorのインポートが必要
  • Database.hsは日時関連が変更されているの修正が必要

Database.hsの日時関連の処理は上記のGitHubのソースを参考に直してみた。

  • インポートに下記を追加
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import Data.Time (UTCTime)
  • 日時の型変換(UTCTime -> ClockTime)
mtime path = toCalendarTime =<< getModificationTime path

mtime :: String -> IO CalendarTime
mtime path = toCalendarTime . uToC =<< getModificationTime path

uToC :: UTCTime -> ClockTime
uToC t = TOD ( truncate . utcTimeToPOSIXSeconds $ t) 0

ここではgetModificationTime関数で得た値(UTCTime)を直接toCalendarTime関数に渡さず、ClockTimeに変換して渡している

toCalendarTime :: ClockTime -> IO CalendarTime
getModificationTime :: FilePath -> IO UTCTime

コメント

  • ここら辺の修正方法はどこで見つけたのだろうか?
    → 公式なマイグレーションガイドなどがある?

その他

今回はサンプルを動かすところで時間が来たので、本の名内容は次回に持ち越し。

前もってサンプルを動かせるようにした方が良いかも?

2016-01-04 『ふつうのHaskellプログラミング』 もくもく読書会(第11章) の振り返り

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門

  • 作者: 青木峰郎,山下伸夫
  • 出版社/メーカー: SBクリエイティブ
  • 発売日: 2014/10/05
  • メディア: オンデマンド (ペーパーバック)
  • この商品を含むブログを見る

第11章 モナド

モナドとは何か

  • Monadクラスのインスタンスになっているものとして、IO、Maybe、リストがある
  • モナドには演算をつなぐという目的がある

Monadクラス

class Monad where
  (>>=) :: ma -> (a -> m b) -> m b
  return :: a -> m a
  • (>>=)はバインドと読む
  • Monad(>>=)returnを実装しているだけでなくモナド則を満たしていなければならない

モナド

  • (return x) >>= f == f x
  • m >>= return == m
  • (m >>= f) >> g = m >>= (\x -> f x >>= g)

Maybeモナド

data Maybe a = Nothing | Just a deriving (Eq, Ord)
  • 値は「Nothing」か「Just x」のどちらか
  • 対象の値がある場合、「Just x」
  • 対象の値がない場合、「Nothing」

alist = association list = 2要素タプルのリスト

Maybeモナドの目的

  • Maybe を返す関数を連続して使わなければならないときに便利
  • 関数を連続して適用してるうちに結果が途中でNothingになると、それ以降の関数で適用してもNothingとして返ってくる

Maybeモナドの実例

本のサンプルでは以下の処理をしている

  1. lookup関数を使ってconfigから"database"を検索し結果を取得
  2. 1の結果からさらにlookup関数を使って"encoding"を検索
case (lookup "database" config) of
  Just entries -> lookup "encoding" entries
  Nothing -> Nothing

これを(>>=)で下記に書き換えれる

lookup "database" config >>= lookup "encoding"
メモ

GHCiで複数行を書く場合は:{:}で囲む。

例:

> :{
> case (lookup "database" config) of
>   Just entries -> lookup "encoding" entries
>   Nothing -> Nothing
> :}

Maybeモナドの仕組み

Mayboeモナドの(>>=)の実装

instance Monad Maybe where
  (Just x) >>= f = f x
  Nothing >>= f = Nothing
  …略

lookupの例の場合、fが「lookup "encoding"」になる

returnクラスメソッドの使い道

returnの定義

> :t return
return :: Monad m => a -> m a

Mayboeモナドのreturnの実装

instance Monad Maybe where
  return x = Just x
  …略
  • 値aを渡したら「Just a」を返す
  • (>>=)で連結している関数の中で結果にモナドを返さないものがある場合に使える

リストモナド

リストモナドの目的

  • リストモナドの目的は適用するたびに値の数が増えたり減ったりする関数を連結すること

リストモナドの実例

本のサンプルではファイル名のパターンを展開する。以下の処理をする

  1. []を使って例えば「img[01].png」とあれば「img0.png」、「img1.png」ができる
  2. {}を使って例えば「img.{png,jpg}」とあれば「img.png」、「img.jpg」ができる

上記の関数として次のものがある(実装は第13章)

  • 1の実装はexpandCharClassでされている
  • 2の実装はexpandAltWordsでされている

Maybeモナドの仕組み

リストモナドの実装

instance Monad [] where
  xs >>= f = conactMap f xs
  return x = [x]

IOモナド

IOモナドの目的

  • IOモナド = アクション
  • 遅延評価が基本のHaskellでアクションが評価される順序を確実に指定できるようにする
  • 参照透過性を保ったまま言語に副作用を持ち込む

IOモナドの概念

  • IOモナドは入出力を実行していない世界
  • IOモナドが評価されると次の2つが生成される
  • 入出力結果
  • 入出力を実行した後の世界

本の説明だとgetContentsアクションの結果「cs」をputStr csで出力する例で説明している

main = do cs <- getContents
          putStr cs

もしくは

main = getContents >>= putStr
  • 入力値「cs」を得るためにgetContentsが先に評価されることを指定している
  • getContentsが実行される前の世界とputStrが実行された後の世界は別の値とHaskellでは考える
    → そのためHaskellでは副作用が発生していないと考える

つまりgetContentの場合だと

  • 入出力結果 = 読み込んだ文字列
  • 入出力を実行した後の世界 = 現状のまま

となり、putStrの場合だと

  • 入出力結果 = なし
  • 入出力を実行した後の世界 = 文字列の表示

ということか?
※ 入出力結果と言うより関数の処理の結果としての値と言うことかと思われる

> :t getContents
getContents :: IO String
> :t putStr
putStr :: String -> IO ()

IOモナドと(>>)

下記のように書き換えれる

do putStrLn "Hello, world"
   putStrLn "Hello, again!!!"

putStrLn "Hello, World" >>= (\x -> putStrLn "Hello, again!!!")

putStrLn "Hello, World" >> putStrLn "Hello, again!!!"

入出力以外でのIOモナド

  • IOモナドは入出力以外でも使われることがある
  • 副作用が必要なものに使える
  • 例としてランダム値や現在時刻やOSとのやりとりなどにIOモナドが使われる

    モナドの構文

    do式

  • (>>=)を使った式は全てdo式に書き換えれる

let節

do式内でlet節を使う場合に注意が必要

do let n = 2 * 16
   in print n
  • インデントをletのlより右にすればOK
do let n = 2 * 16
    in print n
  • インデントをletのlに合わす場合は「in」を除くとOK
do let n = 2 * 16
   print n

do式とif

本ではインデントをifの「i」に合わすとコンパイルエラーとあるがGHCiで試したら問題なかった。 kome

> :}
| do if True
|    then putStrLn "True"
|    else putStrLn "False"
| :}

ファイルにして読み込んでも問題なし

main = do if True
          then putStrLn "True"
          else putStrLn "False"

練習問題

答えの内容で問題なし