├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── Setup.hs ├── app └── Main.hs ├── appveyor.yml ├── assets ├── 1.hs ├── 1.md ├── 2.5.md ├── 2.hs ├── 2.md ├── 3.hs ├── 3.md ├── 4.hs ├── 4.md ├── 5.hs ├── 5.md ├── exercise.css └── goal.hs ├── package.yaml ├── scripts ├── gen-error-messages.hs ├── gen-test-cases.sh └── gen-wrong-sources-from-template.sh ├── src ├── Education │ ├── MakeMistakesToLearnHaskell.hs │ └── MakeMistakesToLearnHaskell │ │ ├── Diagnosis.hs │ │ ├── Env.hs │ │ ├── Error.hs │ │ ├── Evaluator │ │ ├── Regex.hs │ │ ├── RunHaskell.hs │ │ └── Types.hs │ │ ├── Exercise.hs │ │ ├── Exercise │ │ ├── Core.hs │ │ ├── Ex01.hs │ │ ├── Ex02.hs │ │ ├── Ex02_5.hs │ │ ├── Ex03.hs │ │ ├── Ex04.hs │ │ ├── Ex05.hs │ │ ├── FormatMessage.hs │ │ ├── Record.hs │ │ └── Types.hs │ │ └── Text.hs ├── imports │ └── external.hs └── test │ └── imports │ └── external.hs ├── stack.yaml └── test ├── Education ├── MakeMistakesToLearnHaskell │ ├── Exercise1Spec.hs │ ├── Exercise2Spec.hs │ ├── Exercise3Spec.hs │ ├── Exercise4Spec.hs │ ├── SpecEnv.hs │ └── SpecHelper.hs └── MakeMistakesToLearnHaskellSpec.hs ├── Spec.hs └── assets ├── 1 ├── error-messages │ ├── no-main.txt │ ├── single-quote.txt │ └── typo.txt ├── no-main.hs ├── single-quote-no-main.hs ├── single-quote.hs └── typo.hs ├── 2 ├── error-messages │ ├── correct.txt │ ├── no-close-paren.txt │ ├── no-main.txt │ ├── no-number-1.txt │ ├── no-number-2.txt │ ├── no-number-3.txt │ ├── no-open-paren.txt │ ├── no-paren.txt │ ├── no-slash.txt │ ├── no-star.txt │ ├── template.txt │ ├── typo.txt │ └── wrong-number.txt ├── no-close-paren.hs ├── no-main.hs ├── no-number-1.hs ├── no-number-2.hs ├── no-number-3.hs ├── no-open-paren.hs ├── no-paren.hs ├── no-slash.hs ├── no-star.hs ├── typo.hs └── wrong-number.hs ├── 3 ├── error-messages │ ├── correct.txt │ ├── incornsistent-indent.txt │ ├── incornsistent-indent1.txt │ ├── incornsistent-indent2.txt │ ├── no-do.txt │ ├── no-main.txt │ ├── single-quote.txt │ ├── typo.txt │ └── wrong-output.txt ├── incornsistent-indent.hs ├── incornsistent-indent1.hs ├── incornsistent-indent2.hs ├── no-do.hs ├── no-main.hs ├── single-quote.hs ├── typo.hs └── wrong-output.hs ├── 4 ├── equal-no-space.hs ├── equal.hs ├── error-messages │ ├── equal-no-space.txt │ ├── equal.txt │ ├── incornsistent-indent1.txt │ ├── incornsistent-indent2.txt │ ├── main-arrow.txt │ ├── no-arrow.txt │ ├── no-close-paren.txt │ ├── no-do.txt │ ├── no-main.txt │ ├── no-open-paren1.txt │ ├── no-open-paren2.txt │ ├── no-open-paren3.txt │ ├── no-paren.txt │ ├── no-paren1.txt │ ├── no-paren2.txt │ ├── no-paren3.txt │ └── wrong-output.txt ├── incornsistent-indent1.hs ├── incornsistent-indent2.hs ├── main-arrow.hs ├── no-arrow.hs ├── no-close-paren.hs ├── no-do.hs ├── no-main.hs ├── no-open-paren1.hs ├── no-open-paren2.hs ├── no-open-paren3.hs ├── no-paren.hs ├── no-paren1.hs ├── no-paren2.hs ├── no-paren3.hs └── wrong-output.hs └── common └── empty.hs /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | cabal-helper*.build 3 | /tmp/ 4 | 5 | *.exe 6 | *.o 7 | *.hi 8 | *.cabal 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: generic 3 | cache: 4 | timeout: 360 5 | directories: 6 | - "$HOME/.stack/" 7 | - "$HOME/.local/bin/" 8 | - ".stack-work/" 9 | install: 10 | - mkdir -p ~/.local/bin 11 | - export PATH=$HOME/.local/bin:$PATH 12 | - travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 13 | jobs: 14 | include: 15 | - stage: build dependencies 16 | script: stack --no-terminal --install-ghc test --bench --only-dependencies 17 | - stage: run test 18 | script: stack --no-terminal test --bench --no-run-benchmarks --no-haddock-deps --pedantic 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Yuji Yamamoto (c) 2017 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 現在は[`ex6`](https://github.com/haskell-jp/makeMistakesToLearnHaskell/tree/ex6)というブランチで、課題6以降の執筆を続けています 2 | 3 | 興味がある方は是非ご覧ください。こちらが`master`にマージされるのは、課題22の文面まで一通り埋まってからです。あしからず。 4 | 5 | # Make Mistakes to Learn Haskell! 6 | 7 | 作りながら学ぶHaskell入門 8 | 9 | ## 💾インストール方法 10 | 11 | ### まだ[Stack](https://haskellstack.org)や[Haskell Platform](https://www.haskell.org/platform/)をインストールしていない場合は 12 | 13 | [Stack](https://haskellstack.org)のインストールを推奨します。 14 | 下記のいずれかの方法でインストールしてください。 15 | 16 | #### 🍎🐧Mac OS XやLinuxなどのUnix系OSをお使いの方: 17 | 18 | 「ターミナル」を起動し、下記のいずれかのコマンドを実行してください。 19 | 20 | ``` 21 | curl -sSL https://get.haskellstack.org/ | sh 22 | ``` 23 | 24 | あるいは、 25 | 26 | ``` 27 | wget -qO- https://get.haskellstack.org/ | sh 28 | ``` 29 | 30 | #### 🏁Windowsをお使いの方 31 | 32 | [64bit版のWindowsをお使いの方はこちら](https://get.haskellstack.org/stable/windows-x86_64-installer.exe)からインストーラーをダウンロードして、インストールしてください(よくわからなければ、とりあえず64bit版を試してみてください。32bit版のWindowsをお使いの方は残念ながらあきらめてLinuxを仮想マシンで動かしてインストールしてください)。 33 | 34 | [Chocolatey](https://chocolatey.org/)をお使いの方は、 35 | 36 | ``` 37 | choco install haskell-stack 38 | ``` 39 | 40 | でもインストールできます。 41 | 42 | ### 💾GHCと「Make Mistakes to Learn Haskell!」のインストール方法 43 | 44 | 現状はHackageにまだ公開していないので👇のコマンドを実行してください(どのOSでもこのコマンドです)。 45 | 46 | ``` 47 | git clone https://github.com/haskell-jp/makeMistakesToLearnHaskell 48 | # あるいは git clone git://github.com/haskell-jp/makeMistakesToLearnHaskell 49 | 50 | cd makeMistakesToLearnHaskell 51 | stack install 52 | ``` 53 | 54 | GHCのインストールと、「Make Mistakes to Learn Haskell!」のビルドが始まります。 55 | 56 | インストールが完了したら、「⚙️使い方」の節に書かれたコマンドを試してみてください。 57 | 58 | なお、Linuxにおいて、libtinfoパッケージがないとインストールできない」というトラブルが報告されています。 59 | 例えばUbuntuの場合、下記のコマンドを実行してインストールしておく必要があるかも知れません。 60 | 61 | ``` 62 | sudo apt-get install libtinfo-dev 63 | ``` 64 | 65 | #### ⚠️トラブルが発生したら: 66 | 67 | インストール中などに何か困ったことが発生した場合、下記のいずれかのウェブサービスで質問してみてください。 68 | 69 | - [teratailのHaskellタグ](https://teratail.com/tags/Haskell) 70 | - [スタック・オーバーフローのHaskellタグ](https://ja.stackoverflow.com/questions/tagged/haskell) 71 | - [日本Haskellユーザーグループ (a.k.a. Haskell-jp)の公式Slack Workspaceにおける、questionsチャンネル](https://haskell-jp.slack.com/messages/C5666B6BB/) 72 | - [登録はこちらから](https://join.slack.com/t/haskell-jp/shared_invite/enQtNDY4Njc1MTA5MDQxLTAzZGNkZDlkMWYxZDRlODI3NmNlNTQ1ZDc3MjQxNzg3OTg4YzUzNmUyNmU5YWVkMjFmMjFjYzk1OTE3Yzg4ZTM) 73 | - [Redditのr/haskell\_jp](https://www.reddit.com/r/haskell_jp/) 74 | 75 | ### 🆙「Make Mistakes to Learn Haskell!」自体のアップデート方法 76 | 77 | 現状、当入門はまだまだ完成度が低いため、今後も度々更新することとなります。 78 | アップデートが必要な場合は、👆の手順で`git clone`したディレクトリーに移動した上で、 79 | 80 | ``` 81 | git pull 82 | stack install 83 | ``` 84 | 85 | を実行してください。 86 | 87 | ## ⚙️使い方 88 | 89 | 1. 課題の一覧を表示する(特に引数を与えなければ一覧が表示されます) 90 | ``` 91 | mmlh 92 | ``` 93 | 1. 課題1の内容を表示する 94 | ``` 95 | mmlh show 1 96 | ``` 97 | 98 | - ブラウザーが起動してHTMLで課題の内容が表示されるはずですが、うまく行かない場合はターミナル上で表示させるため、次のように`--terminal`オプションを付けて実行してください。 99 | ``` 100 | mmlh show --terminal 1 101 | ``` 102 | 1. 課題の回答をテストする 103 | (最後に`mmlh show`した課題のテストをする) 104 | ``` 105 | mmlh verify your_answer.hs 106 | ``` 107 | 108 | ### 🏁Windowsユーザー向けTips 109 | 110 | MSYS2を利用している場合、stackがインストールするMSYS2と衝突して問題が起こることがあります。その場合、`%APPDATA%\stack\config.yaml`に以下の行を追加し、インストール済みのMSYSを使うように設定しましょう。 111 | 112 | ``` 113 | skip-msys: true 114 | ``` 115 | 116 | ## 開発に協力していただける方へ 117 | 118 | ### 現状 119 | 120 | 問題を表示したり、ユーザーが与えた回答を判定するための基本的なフレームワークはできています。 121 | 私 igrepが社内の勉強会で使用するため、下記のことに取り組んでいます。 122 | 123 | - [計算アプリケーションを作る(概要)](https://github.com/haskell-jp/makeMistakesToLearnHaskell/blob/master/assets/2.5.md)に書いた目標に従い、課題とその判定処理の実装。 124 | - 「そのために以下の課題を解いて、Haskellの初歩を身につけましょう」以降に書いた各課題とその判定処理を実装します(実際には課題の中身を優先して作っています)。 125 | - その他[Issues](https://github.com/haskell-jp/makeMistakesToLearnHaskell/issues)をご覧ください。 126 | 127 | #### 課題におけるプロンプト表記について 128 | 129 | - GHCi に対する入力は `ghci>` 130 | - シェルに対する入力は `shell>` 131 | 132 | と書きましょう。 133 | 134 | #### 特に私 igrepが行いたいこと 135 | 136 | - 課題の内容とその文章を書く 137 | 138 | #### 特に私 igrep以外にやっていただきたいこと 139 | 140 | - 課題の判定処理の実装。 141 | - ヒントの出し方を含みます。詳細は次のセクションをご覧ください。 142 | - その他[Issues](https://github.com/haskell-jp/makeMistakesToLearnHaskell/issues)に書いたこと。 143 | 144 | ##### 課題の判定処理の実装方法 145 | 146 | 主に編集するファイルは[src/Education/MakeMistakesToLearnHaskell/Exercise.hs](https://github.com/haskell-jp/makeMistakesToLearnHaskell/blob/master/src/Education/MakeMistakesToLearnHaskell/Exercise.hs)です。 147 | このファイルに各課題の判定方法や、ヒントの判定処理がすべてまとまっています。 148 | 文字列まわりで共通化できる処理を追加したくなったら、`Exercise.hs`に直接追加するか、[`src/Education/MakeMistakesToLearnHaskell/Evaluator/`](https://github.com/haskell-jp/makeMistakesToLearnHaskell/tree/master/src/Education/MakeMistakesToLearnHaskell/Evaluator)以下にファイルを追加するのが良いかと思います(おいおい`Regex.hs`というファイルを追加します)。 149 | 150 | 課題の判定処理のテストを書く場合は、[test/Education/MakeMistakesToLearnHaskell/](https://github.com/haskell-jp/makeMistakesToLearnHaskell/tree/master/test/Education/MakeMistakesToLearnHaskell)ディレクトリーに、`ExerciseNSpec.hs`(`N`は整数)という名前のファイルを追加してください。 151 | 具体的なテストの書き方は既存のファイルを見ていただくとわかるかと思います。Hspecを使っています。 152 | 153 | ### 注意事項 154 | 155 | - このアプリケーションは、Haskellの初心者でもソースコードを編集できるよう、できるだけ素朴なHaskellで書くことを目指しています。 156 | 具体的な基準は適宜議論しようと思いますが、現状は例えば 157 | - 使用方法が比較的分かりやすい(かつ必要性が高い)`default-extensions`に記載した言語拡張のみを使う 158 | - いわゆる `ReaderT IO` パターンを使用せず、 `Env` を受け取る関数を全体で使用する 159 | - Monad Transformerを極力使わない(局所的には使うかも) 160 | - 型レベルプログラミングをしない 161 | - その一方、**`CPP`を使うことで`import`を[`src/imports/external.hs`](https://github.com/haskell-jp/makeMistakesToLearnHaskell/blob/master/src/imports/external.hs)にまとめる**、というかなり変わったことをしています。 162 | これは、コードベース内で`import`の書き方を統一する、同じ`import`を何度も書かないで済ますために考えた方策です。 163 | 現状、`external.hs`で外部のパッケージの`import`のみをまとめていますが、今後もっとコードベースが大きくなったとき、レイヤーを分ける際にも役立つでしょう。 164 | そのため、今後も少なくとも外部のパッケージを新たに`import`する際は、**必ず[`src/imports/external.hs`](https://github.com/haskell-jp/makeMistakesToLearnHaskell/blob/master/src/imports/external.hs)に**書いてください。 165 | 166 | ## ターゲット 167 | 168 | - プログラミングは他の言語で経験している。 169 | - 他のHaskell入門書を読んでみたが、Haskellでプログラムを書く方法がわからない。 170 | - TODO: にしては今の内容はちょっと初歩的すぎるかもしれないので、このターゲット自体かexerciseの内容を改めよう 171 | - Haskellがどんな言語か、どうやってプログラムを作るのか、軽く知りたい。 172 | 173 | ## 依存ライブラリ 174 | 175 | - [main-tester](https://gitlab.com/igrep/main-tester) 176 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | 4 | import qualified Education.MakeMistakesToLearnHaskell 5 | 6 | 7 | main :: IO () 8 | main = Education.MakeMistakesToLearnHaskell.main 9 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Disabled cache in hope of improving reliability of AppVeyor builds 2 | #cache: 3 | #- "c:\\sr" # stack root, short paths == fewer problems 4 | 5 | build: off 6 | 7 | init: 8 | - ps: Set-WinSystemLocale ja-JP 9 | - ps: Start-Sleep -s 5 10 | - ps: Restart-Computer 11 | 12 | before_test: 13 | # http://help.appveyor.com/discussions/problems/6312-curl-command-not-found 14 | - set PATH=C:\Program Files\Git\mingw64\bin;%PATH% 15 | 16 | - curl -sS -ostack.zip -L --insecure https://get.haskellstack.org/stable/windows-x86_64.zip 17 | - 7z x stack.zip stack.exe 18 | 19 | clone_folder: "c:\\stack" 20 | environment: 21 | global: 22 | STACK_ROOT: "c:\\sr" 23 | TMP: "c:\\tmp" 24 | 25 | cache: 26 | - "c:\\sr" 27 | 28 | test_script: 29 | - chcp 932 30 | - stack setup > nul 31 | # The ugly echo "" hack is to avoid complaints about 0 being an invalid file 32 | # descriptor 33 | - echo "" | stack --no-terminal test --jobs 1 --pedantic 34 | -------------------------------------------------------------------------------- /assets/1.hs: -------------------------------------------------------------------------------- 1 | main = putStrLn "Hello, world!" 2 | -------------------------------------------------------------------------------- /assets/1.md: -------------------------------------------------------------------------------- 1 | # Hello, world! 2 | 3 | まずははじめの一歩として、`Hello, world!`という文字列を標準出力に書き込むプログラムを書きましょう。 4 | 5 | ## 課題のプロンプト表記について 6 | 7 | 今後の課題ではプロンプトが2種類出てきます。 8 | そのため、課題を進めていく上でどちらのプロンプトか曖昧にならないように、以下のルールでプロンプトを表記しています。 9 | 10 | - GHCi に対する入力は `ghci>` 11 | - シェル に対する入力は `shell>` 12 | 13 | # 必要な知識 14 | 15 | ## Haskellのプログラムの書き方 16 | 17 | Haskellでプログラムを書くには、`main`という名前の関数の定義を、`.hs`という拡張子のファイルに書く必要があります。 18 | 19 | ```haskell 20 | main = putStrLn "A string!" 21 | ``` 22 | 23 | 上記は、`"A string!"`という文字列を標準出力に出力する、Haskell製のプログラムのソースコードです。たったの1行ですね! 24 | `putStrLn`という関数が、文字列を受け取ってその文字列を標準出力に出力します。 25 | 26 | 「main関数」を定義する他の言語を経験された方から見ると、`main = ...`と、`=`を使用する辺り、ちょっと変わった書き方に見えるかもしれません。 27 | Haskellではこのように、変数に値を代入するのと同じような書き方で、関数も定義します。 28 | 29 | ## 課題を答え合わせする方法 30 | 31 | 課題「`Hello, world!`という文字列を標準出力に書き込むプログラム」が書けたら、`mmlh verify`コマンドで実行してみましょう。 32 | 33 | ``` 34 | shell> mmlh verify program.hs 35 | ``` 36 | 37 | `mmlh verify`コマンドは、指定したHaskellのソースコードを実行して、実際に正しく動作するかテストしてくれます。 38 | さらに、間違った回答をした場合に、エラーメッセージやソースコードを解析して、ヒントを出してくれることもあります。 39 | これから紹介する「普通に実行する方法」よりも学習の助けになるはずです。ぜひ積極的に使ってください! 40 | 41 | ## 普通に実行する方法 42 | 43 | もちろん、答え合わせなしで普通に実行する方法もあります。 44 | Haskellのプログラムを実行する方法はいくつかありますが、ここではお手軽に`runhaskell`コマンドを使いましょう。 45 | 例えば`program.hs`という名前でソースコードを保存した場合、次のように実行してください。 46 | 47 | ``` 48 | shell> runhaskell program.hs 49 | ``` 50 | 51 | GHCをstackで入れた、という場合は`stack exec runhaskell`コマンドを使いましょう。 52 | 53 | ``` 54 | shell> stack exec runhaskell program.hs 55 | ``` 56 | -------------------------------------------------------------------------------- /assets/2.5.md: -------------------------------------------------------------------------------- 1 | # 計算アプリケーションを作る(概要) 2 | 3 | ここから先は、次のような仕様のコマンドラインアプリケーションを作ることをゴールとします。 4 | 5 | - 以下のような内容の、1行ごとにタブ区切りで「分類(タブ文字や改行を含まない文字列)」と「金額(0以上の整数)」が書かれたファイルの名前を(0個以上)引数として受け取る(``はタブ文字、``は改行文字を表す)。 6 | ``` 7 | 分類1234 8 | ほかの分類9876 9 | さらにほかの分類3141592 10 | ``` 11 | - ファイルを読み、「分類」ごとの「金額」の合計を計算し、出力する。 12 | - 同じ「分類」の行が受け取ったファイル内に複数存在していることがあるので、「分類」ごとの「金額」の合計を知りたい。 13 | 14 | そのために以下の課題を解いて、Haskellの初歩を身につけましょう。 15 | 16 | - 複数の命令を並べる 17 | - 入力の取得 18 | - 入力を数値に変換する 19 | - タプルから値を取り出す 20 | - 入力した値の内容に応じて処理を分ける 21 | - `case`式 22 | - エラーを出す・無理矢理コンパイルを通す 23 | - moduleをimportする 24 | - base以外のパッケージを利用する 25 | - 処理を繰り返し実行する 26 | - メモ: 再帰関数に入るか、それとも高階関数をするか 27 | - 連想配列を扱う 28 | 29 | さらに、作ったアプリケーションの改善を通して、以下のことを学びます。 30 | 31 | - 自分で型を定義する 32 | - エラーを型宣言で明示する 33 | -------------------------------------------------------------------------------- /assets/2.hs: -------------------------------------------------------------------------------- 1 | main = print (60 / (1.7 * 1.7)) 2 | -------------------------------------------------------------------------------- /assets/2.md: -------------------------------------------------------------------------------- 1 | # 数値の計算・表示 2 | 3 | 身長1.7m, 体重60kgの人のBMIを計算して、標準出力に出力するプログラムを書きましょう。 4 | 5 | ## 必要な知識 6 | 7 | ### 計算式 8 | 9 | 身長1.7m, 体重60kgの人のBMIは、以下の計算式で求めることができます。 10 | 11 | ``` 12 | 60 ÷ ( 1.7 × 1.7 ) 13 | ^^^^ ^^^^ ^^^^ 14 | 体重 ÷ (身長 × 身長) 15 | ``` 16 | 17 | ### 数の計算 18 | 19 | ほかのプログラミング言語と同様、Haskellでも各種四則演算用の演算子(`+`・`-`・`*`・`/`)が使用できます。 20 | 簡単にそのことを試すために、GHCiというアプリケーションを使ってみましょう。 21 | 22 | ### GHCiを電卓として使う 23 | 24 | GHCiはいわゆる「REPL (Read Evaluate Print Loop)」と言われる類いのアプリケーションです。 25 | Haskellの式を入力することで、式を評価した結果を簡単に確認することができます。 26 | 27 | ```bash 28 | shell> ghci 29 | GHCi, version 8.2.2: http://www.haskell.org/ghc/ :? for help 30 | ghci> 1 + 1 31 | 2 32 | ``` 33 | 34 | GHCiを`stack`を通して使う場合は、前の課題の`stack exec runhaskell`と同様に、`stack exec ghci`と入力してください。 35 | 36 | ```bash 37 | shell> stack exec ghci 38 | GHCi, version 8.2.2: http://www.haskell.org/ghc/ :? for help 39 | ghci> 1 / 3 40 | 0.3333333333333333 41 | ``` 42 | 43 | 終了する場合は `:q` と入力してください。 44 | 45 | ``` 46 | ghci> :q 47 | ``` 48 | 49 | #### コラム: `stack ghci`か`stack exec ghci`か 50 | 51 | 実は、ここで紹介した`stack exec ghci`のほかに、`stack ghci`というサブコマンドが`stack`にはあります。 52 | `stack ghci`は`stack exec ghci`とよく似た機能ですが、`stack exec ghci`の方が場合によっては使いやすいので、ここでは`stack exec ghci`を採用しています(詳細は申し訳なくも割愛します!)。 53 | 54 | 同様に、前の課題で紹介した`stack exec runhaskell`にも、より短い`stack runhaskell`というコマンドがあります。 55 | 残念ながら、`stack runhaskell`については、「ラップしている`runhaskell`コマンドに直接オプションを渡せない」という大きな落とし穴があり、あまりおすすめできるものではありません。 56 | 加えて`stack exec ghci`と一貫させるためにも、`stack exec runhaskell`を使用しています。 57 | 58 | タイプ数が少し多くなってしまいますが、ご容赦ください。 59 | 60 | ### GHCも電卓として使う 61 | 62 | 実はGHCiだけでなく、GHCだけでも電卓のように使うことができます。 63 | 以下のように実行してみてください。 64 | 65 | ``` 66 | shell> ghc -e "2 * 4" 67 | ``` 68 | 69 | `ghc`コマンドの`-e`オプションは、GHCiに入力するのと同じようにHaskellの式を指定することで、式の評価結果を簡単に確認することができます。 70 | 1回だけ計算を行いたい場合や、ワンライナーを書きたい場合に便利な機能です。 71 | 72 | stackを通して利用する場合は、オプションの指定方法に気をつけてください。 73 | 74 | ``` 75 | shell> stack exec ghc -e "2 * 4" 76 | Invalid option `-e' 77 | 78 | Usage: stack.exe exec CMD [-- ARGS (e.g. stack exec -- ghc-pkg describe base)] 79 | ([--plain] | [--[no-]ghc-package-path] [--[no-]stack-exe] 80 | [--package ARG] [--rts-options RTSFLAG] [--cwd DIR]) 81 | [--help] 82 | Execute a command 83 | ``` 84 | 85 | これは`stack`コマンド(をはじめ、[optparse-applicative](https://hackage.haskell.org/package/optparse-applicative)を使ったオプションパーサー)の悩ましい仕様で、`ghc`コマンドに渡したつもりのオプションが、`stack`コマンドによって解釈されてしまうために起こるエラーです。 86 | 87 | 正しく`ghc`コマンドに`-e`オプションを渡すには、`-e`オプションの手前に`--`を挟みましょう。 88 | 89 | ``` 90 | shell> stack exec ghc -- -e "2 * 4" 91 | 8 92 | ``` 93 | 94 | ## 数の出力 95 | 96 | 前の課題で紹介した`putStrLn`は、文字列のみしか出力できません。`1`や`3`などの数を出力することができないのです。 97 | これもGHCiや先ほどの`ghc -e`を使うことで確認することができます。 98 | 99 | ``` 100 | shell> stack exec ghci 101 | ghci> putStrLn 4 102 | 103 | :2:10: error: 104 | ? No instance for (Num String) arising from the literal ‘4’ 105 | ? In the first argument of ‘putStrLn’, namely ‘4’ 106 | In the expression: putStrLn 4 107 | In an equation for ‘it’: it = putStrLn 4 108 | ``` 109 | 110 | 代わりに、`print`という関数を使いましょう。 111 | 112 | ``` 113 | shell> stack exec ghci 114 | ghci> print 4 115 | 4 116 | ``` 117 | 118 | `print`は数以外の様々な値も出力できるようになっています(詳細はこの先の課題でいつか!)。 119 | GHCiや`ghc -e`オプションが、入力した結果を出力する際にも`print`は内部で使われています。 120 | つまり、先ほどの`print 4`で出力される結果は、GHCiに`4`を入力した結果と同じなのです。 121 | 122 | `print`を文字列に使った結果どうなるかは... 試してみてください。 123 | 124 | ## 式を括弧で囲う 125 | 126 | Haskellはほかの言語と異なり、**関数呼び出しが最も優先して結合**されるという仕様となっています。ちょっと変わっていますね。 127 | ここまで紹介した`print`関数や`putStrLn`関数も当然例外ではなく、例えば`17 / 3`を計算した結果を出力するつもりで下記の式を入力すると、エラーになってしまいます。 128 | 129 | ``` 130 | ghci> print 17 / 3 131 | 132 | :1:1: error: 133 | ? No instance for (Fractional (IO ())) arising from a use of ‘/’ 134 | ? In the expression: print 17 / 3 135 | In an equation for ‘it’: it = print 17 / 3 136 | 137 | :1:12: error: 138 | ? No instance for (Num (IO ())) arising from the literal ‘3’ 139 | ? In the second argument of ‘(/)’, namely ‘3’ 140 | In the expression: print 17 / 3 141 | In an equation for ‘it’: it = print 17 / 3 142 | ``` 143 | 144 | わかりやすくするためにカッコを補いましょう。 145 | Haskellは`print 17 / 3`という式を次のように解釈しています。 146 | 147 | ``` 148 | ghci> (print 17) / 3 149 | ``` 150 | 151 | `(print 17)`の結果を`3`で割ろうとしていることがわかるでしょうか? 152 | `(print 17)`という式は`IO ()`というちょっと変わった型の値を返します。 153 | 「`IO ()`とはなんぞや」という話も含め、ここでは詳細を割愛しますが、上記のエラーメッセージでは、大まかに次のことを伝えています。 154 | 155 | - (`(print 17)`という式が返す)`IO ()`という型の値は、`/`という演算子を使うために必要な条件を満たしていません。 156 | - (`(print 17)`という式が返す)`IO ()`という型の値は、`3`という数値リテラルで表現できる値ではありません。 157 | 158 | これらのエラーメッセージを一瞥しただけで慌てず自力で解決方法を探れるようになったら、あなたはすごいHaskellerです。 159 | これから課題を解いてみて、**間違えながら**学んでいきましょう! 160 | 161 | ## 答え合わせをする方法 162 | 163 | 課題1でやったことを思い出しながら、「身長1.7m, 体重60kgの人のBMIを計算」して出力するプログラムを書いてみましょう。 164 | 書けたら、課題1と同様、`mmlh verify`コマンドで答え合わせしましょう。 165 | 166 | ``` 167 | shell> mmlh verify 2.hs 168 | ``` 169 | -------------------------------------------------------------------------------- /assets/3.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | putStrLn "# # ####### # # #####" 3 | putStrLn "# # # # # # #" 4 | putStrLn "# # # # # # #" 5 | putStrLn "####### ##### # # # #" 6 | putStrLn "# # # # # # #" 7 | putStrLn "# # # # # # #" 8 | putStrLn "# # ####### ####### ####### #####" 9 | -------------------------------------------------------------------------------- /assets/3.md: -------------------------------------------------------------------------------- 1 | # 複数の命令を並べる 2 | 3 | 下記のような簡単なアスキーアートを標準出力に書き込むプログラムを書きましょう。 4 | 5 | **注: 表示が崩れる場合は、等幅フォントで再度表示してみてください。** 6 | 7 | ``` 8 | # # ####### # # ##### 9 | # # # # # # # 10 | # # # # # # # 11 | ####### ##### # # # # 12 | # # # # # # # 13 | # # # # # # # 14 | # # ####### ####### ####### ##### 15 | ``` 16 | 17 | ## 必要な知識 18 | 19 | 今回の課題を解決する方法はいくつも考えられますが、ここでは2つの方法を紹介します。 20 | 21 | ### 文字列リテラルの中での改行文字 22 | 23 | Haskellの文字列リテラルは、改行文字を直接含めることができません。 24 | 下記のように、GHCiでダブルクォート`"`の中に改行を挟もうとすると、エラーになります。 25 | 26 | ```haskell 27 | ghci> "newline here: 28 | 29 | :11:5: error: 30 | lexical error in string/character literal at end of input 31 | ``` 32 | 33 | 文字列リテラルの中での改行は、必ず `\n` というバックスラッシュ(環境によっては円マークに見えるかも知れません)で始まる、エスケープシーケンスを使用してください。 34 | ほかのプログラミング言語でも馴染みがある文字列かと思います。 35 | 36 | ```haskell 37 | ghci> "newline here:\nafter newline" 38 | "newline here:\nafter newline" 39 | ``` 40 | 41 | 最初の課題で使用した`putStrLn`関数を使えば、改行が混ざったことがよりわかりやすいでしょう。 42 | 43 | ```haskell 44 | ghci> putStrLn "newline here:\nafter newline" 45 | newline here: 46 | after newline 47 | ``` 48 | 49 | ### `do`記法 50 | 51 | 最初の課題で使用した`putStrLn`関数は、渡した文字列の末尾に、自動的に改行文字を加えて出力します。 52 | この特徴を利用すれば、`putStrLn`関数を複数の行に渡って連続して実行することで、`\n` を使った場合よりも、視覚的に分かりやすく複数行の文字列を表示できます。 53 | 54 | ただし、Haskellはちょっと変わっていて、`putStrLn`関数をそのまま複数行書いても、エラーになってしまいます。 55 | 例えば、以下のようなソースを`no-do.hs`という名前のファイルに保存し、`runhaskell`で実行してみます。 56 | 57 | ```haskell 58 | main = 59 | putStrLn "newline here:" 60 | putStrLn "after newline" 61 | ``` 62 | 63 | 下記のようなエラーになるでしょう。 64 | 65 | ``` 66 | shell> stack exec runhaskell no-do.hs 67 | 68 | test\assets\3\no-do.hs:2:3: error: 69 | • Couldn't match expected type ‘(String -> IO ()) -> [Char] -> t’ 70 | with actual type ‘IO ()’ 71 | • The function ‘putStrLn’ is applied to three arguments, 72 | but its type ‘String -> IO ()’ has only one 73 | In the expression: 74 | putStrLn "newline here:" putStrLn "after newline" 75 | In an equation for ‘main’: 76 | main = putStrLn "newline here:" putStrLn "after newline" 77 | • Relevant bindings include 78 | main :: t (bound at test\assets\3\no-do.hs:1:1) 79 | | 80 | 2 | putStrLn "newline here:" 81 | | ^^^^^^^^^^^^^^^^^^^^^^^^... 82 | ``` 83 | 84 | わかりづらいのですが、エラーメッセージの 85 | 86 | ``` 87 | • The function ‘putStrLn’ is applied to three arguments, 88 | but its type ‘String -> IO ()’ has only one 89 | ``` 90 | 91 | という箇所から、「`putStrLn`に渡した引数が多すぎる」ということが読み取れるでしょうか? 92 | つまりHaskellは、最初の`putStrLn`に対して、`"newline here:"`, `putStrLn`, `"after newline"`という、3つの引数を渡した、と解釈したようです。 93 | つまり、他のプログラミング言語の構文で例えると、下記のように`putStrLn`関数を呼び出した、と解釈したのです。 94 | 95 | ``` 96 | putStrLn("newline here:", putStrLn, "after newline") 97 | ``` 98 | 99 | 実際にはこれまでに使ってきたとおり、`putStrLn`は1つの引数しか受け取らない関数なので、これではエラーになってしまいます。 100 | 101 | このエラーを解決する(一つの)方法が、`do`記法です。 102 | `do`記法は、`putStrLn`をはじめとする「命令」を、連続して実行するよう「つなげる」ための便利な構文です。 103 | ここでは詳細に立ち入りませんが、`putStrLn`だけでなく、Haskellのソースコードの様々な箇所で使われます。 104 | あの、「モナド(`Monad`)」を理解する上で非常に重要な機能となっていますので、お楽しみに。 105 | 106 | そんな`do`記法は、下記のように使用します。 107 | 108 | ```haskell 109 | main = do 110 | putStrLn "newline here:" 111 | putStrLn "after newline" 112 | ``` 113 | 114 | 先ほどの間違った例と比べて違うのは、`main =`の後に`do`が書いてあるかないか、それだけです。 115 | 後はほかのよくあるプログラミング言語と似たように、`putStrLn "newline here:"`などの連続して実行したい命令を改行で切って並べれば、並べた命令を続けて実行できます。 116 | それ故に書き忘れてしまい、先ほどのようなわかりづらいエラーになってしまうことも多いので、ご注意ください。 117 | 118 | もちろん、3つ以上命令を並べたいときも同様です。 119 | 120 | ```haskell 121 | main = do 122 | putStrLn "あ" 123 | putStrLn "い" 124 | putStrLn "う" 125 | putStrLn "え" 126 | putStrLn "お" 127 | ``` 128 | 129 | #### GHCi上で`do`記法を試す 130 | 131 | ここまで読んですでに試した方もいらっしゃるかと思うので紹介しておきましょう。 132 | GHCi上で`do`記法をそのまま入力しようとすると、下記のようなエラーになります。 133 | 134 | ```haskell 135 | ghci> do 136 | 137 | :4:1: error: Empty 'do' block 138 | ``` 139 | 140 | これを回避する方法はいくつかありますが、ここでは一番使いやすい`:set +m`を使った方法を紹介します。 141 | 142 | GHCiの設定を変えるコマンド`:set`に`+m`という引数を渡すと、複数行のコマンドが入力できるようになります。 143 | 144 | ``` 145 | ghci> :set +m 146 | ghci| do 147 | ghci| putStrLn "aaa" 148 | ghci| putStrLn "bbb" 149 | ghci| 150 | aaa 151 | bbb 152 | ``` 153 | 154 | ※上記の`ghci| `という表記は、行の続きを入力する際に表示されるプロンプトです。`ghci> `と同様、設定によって違う表示になるかと思います。 155 | 156 | 入力を終了させたい場合は、空行を入力してください。 157 | 158 | `:set +m`を毎回入力するのが面倒だ、という場合は、`~/.ghci`というファイルに、`:set +m`を追記しましょう。 159 | 160 | ## 課題の解き方 161 | 162 | Haskell固有の機能を覚えるため、今回は、必ず`do`記法を使用して解いてください。 163 | -------------------------------------------------------------------------------- /assets/4.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr (unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /assets/4.md: -------------------------------------------------------------------------------- 1 | # 入力の取得 2 | 3 | 標準入力から読んだ文字列を、改行文字で区切って、逆順に並び替えて標準出力に書き込みましょう。 4 | 5 | ## 実行結果例 6 | 7 | `input.txt`という、次のような内容のファイルがあるとき、 8 | 9 | ``` 10 | Lorem ipsum dolor sit amet, 11 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 12 | consectetur adipiscing elit, 13 | ``` 14 | 15 | 下👇のように実行すると、 16 | 17 | ``` 18 | shell> stack exec runhaskell 4.hs < input.txt 19 | ``` 20 | 21 | 下👇のような文字列を出力します。 22 | 23 | ``` 24 | consectetur adipiscing elit, 25 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 26 | Lorem ipsum dolor sit amet, 27 | ``` 28 | 29 | ## 必要な知識 30 | 31 | ### リスト、`reverse`関数、`lines`関数、`unlines`関数、`putStr`関数 32 | 33 | まずは入力や出力をどのように行うのかは忘れて、「文字列に対して、どんな処理をするのか」を考えましょう。 34 | 今回の課題に関して言えばずばり「文字列を、改行文字で区切って、逆順に並び替え」る処理が該当します。 35 | 36 | ちょっと脱線。 37 | Haskellでのプログラミングに限らず、プログラムの設計について考えるときは、可能な限り「入力や出力に関わる部分」と「(入力を受け取ってできた)**値に対して**(最終的に出力する前に)**どのような処理をするのか**」を分けて考えると、より再利用性が高く、テストも理解もしやすいプログラムが書けます。 38 | 例えば今回の課題に関して言えば、そのように考えることで、「文字列を改行文字で区切って、逆順に並び替え」る処理を、GHCi上でもお気軽にテストできます。 39 | 40 | と、言うわけで今回の課題を解くための関数をGHCi上で試しましょう。 41 | 42 | ```bash 43 | shell> stack exec ghci 44 | ``` 45 | 46 | 最初に紹介するのは文字列を「改行文字で区切る」関数、`lines`関数です。 47 | 課題3で触れた、`\n`を思い出しながら、次のように入力してみてください。 48 | 49 | ```haskell 50 | ghci> lines "A string\ncontaining\nnewlines\nin the middle." 51 | ["A string","containing","newlines","in the middle."] 52 | ``` 53 | 54 | 出力された角括弧`[]`で囲われたものは、文字列のリストです。 55 | リストは、Haskellで(同じ型の)複数の値を表すのに使われる、最も単純なデータ構造です。 56 | もちろん文字列だけでなく、ほかのどんな型の値も入れることができます。 57 | 例えば下記は数値のリストです。 58 | 59 | ```haskell 60 | ghci> [1, 2, 3] 61 | [1,2,3] 62 | ``` 63 | 64 | `lines`関数は、受け取った文字列を改行文字(`\n`)で区切ることで、その文字列のリストを作ります。 65 | これで「改行文字で区切って」という処理は実装できますね。 66 | 67 | 続いて、「逆順に並び替え」る処理です。 68 | `lines`関数で作った文字列のリストを逆順にするには、文字通り`reverse`関数を使いましょう。 69 | 70 | ```haskell 71 | ghci> reverse ["A string","containing","newlines","in the middle."] 72 | ["in the middle.","newlines","containing","A string"] 73 | ``` 74 | 75 | 逆順になって返ってきましたね。 76 | 77 | さて、この課題の要件では最終的に「標準出力に書き込」まなければならないのでした。 78 | これまでの課題で勉強した`putStrLn`関数は、文字列を渡さないと型エラーになります。 79 | 80 | ```haskell 81 | ghci> putStrLn ["in the middle.","newlines","containing","A string"] 82 | 83 | :12:11: error: 84 | ? Couldn't match expected type ‘Char’ with actual type ‘[Char]’ 85 | ? In the expression: "in the middle." 86 | In the first argument of ‘putStrLn’, namely 87 | ‘["in the middle.", "newlines", "containing", "A string"]’ 88 | In the expression: 89 | putStrLn ["in the middle.", "newlines", "containing", "A string"] 90 | 91 | ... (以下略) 92 | ``` 93 | 94 | 課題2で紹介した`print`関数は文字列のリストにも使えます。 95 | 96 | ```haskell 97 | ghci> print ["in the middle.","newlines","containing","A string"] 98 | ["in the middle.","newlines","containing","A string"] 99 | ``` 100 | 101 | ただし、これは課題の要件にあった出力ではありません。 102 | 改行文字で区切って逆順にした後、各行を逆順に出力するには、また改行文字で文字列を結合する必要があるのです。 103 | そこで役に立つのが`unlines`関数です。 104 | 105 | ```haskell 106 | ghci> unlines ["A string","containing","newlines","in the middle."] 107 | "A string\ncontaining\nnewlines\nin the middle.\n" 108 | ``` 109 | 110 | 期待したとおり、リストの要素が改行文字で結合されて、一つの文字列に変わったのがわかるでしょうか? 111 | できた文字列を`putStrLn`してみると、よくわかります。 112 | 113 | ```haskell 114 | ghci> putStrLn "A string\ncontaining\nnewlines\nin the middle.\n" 115 | A string 116 | containing 117 | newlines 118 | in the middle. 119 | 120 | ghci> 121 | ``` 122 | 123 | おっと、最後の行`in the middle`の後に、余計な空行ができてしまいました。 124 | これでは課題の要件をクリアできません。 125 | `putStrLn`の`Ln`は「Line」の略であるとおり、必ず行の末尾の文字である`\n`を出力します。 126 | そして、`unlines`も文字列の末尾に`\n`を加えます。 127 | `\n`はあくまでも行の末尾にあるべき文字なので、最後の文字にもちゃんと加えるのでしょう。 128 | 129 | いずれにしても、この問題を回避するためには、`putStr`という文字通り`Ln`がつかない関数を使います。 130 | 131 | ```haskell 132 | ghci> putStr "A string\ncontaining\nnewlines\nin the middle.\n" 133 | A string 134 | containing 135 | newlines 136 | in the middle. 137 | ghci> 138 | ``` 139 | 140 | 今度は余計な空行が出てませんね!LGTM! 141 | 142 | ### 全部組み合わせる 143 | 144 | さて、ここまで紹介した関数を生かして、「文字列を、改行文字で区切って、逆順に並び替え」る処理と、さらにそれを出力する処理を実装してみましょう。 145 | 全部組み合わせてしまうと課題の答えになってしまうので、一部の処理を示すことで、注意すべき点をお伝えしたいと思います。 146 | 147 | 結論から先に書くと、文字列を改行文字`\n`で区切って`print`関数で中身を表示する処理は、次のように書くことで実装できます。 148 | 149 | ```haskell 150 | ghci> print (lines "A string\ncontaining\nnewlines\nin the middle.\n") 151 | ["A string","containing","newlines","in the middle."] 152 | ``` 153 | 154 | `(lines "A string\ncontaining\nnewlines\nin the middle.\n")`と、**関数名とその引数両方をカッコで**囲っている点に注意してください。 155 | ほかのよくあるプログラミング言語のように、 156 | 157 | ```haskell 158 | ghci> print lines("A string\ncontaining\nnewlines\nin the middle.\n") 159 | ``` 160 | 161 | でもなければ、ましてや 162 | 163 | ```haskell 164 | ghci> print lines "A string\ncontaining\nnewlines\nin the middle.\n" 165 | ``` 166 | 167 | でもない点に注意してください。 168 | この場合は、`print`関数に`lines`関数と`"A string\ncontaining\nnewlines\nin the middle.\n"`という**2つの引数**を渡している、という意味になります。 169 | 「`print`関数に`lines`関数を渡す」というのがちょっとおかしく聞こえるかもしれません。 170 | `print`関数以外の関数ですと、Haskellの構文上そういう式も普通にあり得るので、Haskellは2つの引数を渡した、と解釈します。 171 | 172 | いずれにしても、`print`関数は1つの引数しか受け取らないので、上記の式はエラーになります。 173 | 174 | ```haskell 175 | ghci> print lines "A string\ncontaining\nnewlines\nin the middle.\n" 176 | 177 | :23:1: error: 178 | ? Couldn't match expected type ‘[Char] -> t’ 179 | with actual type ‘IO ()’ 180 | ? The function ‘print’ is applied to two arguments, 181 | but its type ‘(String -> [String]) -> IO ()’ has only one 182 | In the expression: 183 | print lines "A string\ncontaining\nnewlines\nin the middle.\n" 184 | In an equation for ‘it’: 185 | it = print lines "A string\ncontaining\nnewlines\nin the middle.\n" 186 | ? Relevant bindings include it :: t (bound at :23:1) 187 | ``` 188 | 189 | `? The function ‘print’ is applied to two arguments,` 190 | `but its type ‘(String -> [String]) -> IO ()’ has only one` という箇所が該当しますので、ぼんやり覚えておいてください。 191 | 192 | ここまでの話を一般化しましょう。 193 | Haskellで関数を呼び出す際は、次のような形式で書いてください。 194 | 195 | ```haskell 196 | 関数名 引数1 引数2 ... 引数N 197 | ``` 198 | 199 | 関数名と引数をスペースで区切っているだけでいいのです。 200 | カッコは、どの関数にどの引数を渡しているのか、明確にしたいときだけ使います。 201 | 202 | ※以下で使用しているように、Haskellで行コメントは`--`で始めます。`--`から行末まではコメントとして扱われ、GHCには解釈されません。 203 | 204 | ```haskell 205 | 関数A Aの引数1 (関数B Bの引数1 Bの引数2 ... Bの引数N) Aの引数3 ... Aの引数N 206 | -- ^------------------------------------^ 207 | -- この部分が関数Aの引数2になる 208 | ``` 209 | 210 | 関数呼び出しは非常に頻繁に使う構文なので、簡単に使えるのはいいことですね! 211 | 212 | 213 | ちなみに、Haskellにはこうした関数呼び出しがもっと複雑に入れ子になった場合に、より簡潔に書く方法があります。 214 | おいおい説明しますね。 215 | 216 | ### `do`記法で`getContents`関数と細い左矢印`<-`を使って、入力を受け取る 217 | 218 | いよいよ、標準入力から文字列を読んで、変数に代入する方法を説明しましょう。 219 | Haskellでこれを一番簡単に実現する方法は、ちょっと変わっています。 220 | 例えば、「`getContents`という命令を使って標準入力から読んだ文字列を、`input`という変数に代入する」という処理は、`do`記法の中で、下記のように書きます。 221 | 222 | ```haskell 223 | do 224 | input <- getContents 225 | ``` 226 | 227 | 変数への代入というと、イコール `=` を使った構文をイメージする方が多いかと思います。 228 | しかし、ここでは細い左矢印 `<-` を使わなければなりません(まだ詳しく説明していませんが、イコール `=`を使った代入もやっぱりあります)。 229 | 230 | Haskellにおいて、`getContents`は、「命令(慣習的に『アクション』と呼ばれることも多いです)」であり、その実行結果を取り出して変数に代入する際、細い左矢印 `<-` を使います。 231 | 232 | これまでに何度も使った、`putStrLn`や`print`、それから`putStr`も「命令」です。 233 | ただし、`putStrLn`や`print`などは特に意味のある結果を返さないので、これまで`<-`は出てきませんでした。 234 | `do`記法は、これらのような「命令」を列挙したり、「命令」の実行結果を簡単に利用するための仕組みを提供してくれるのです。 235 | 236 | 細い左矢印 `<-` を使って代入した変数は、普通の変数と同様に参照して使うことができます。 237 | 238 | ```haskell 239 | do 240 | input <- getContents 241 | putStr input 242 | ``` 243 | 244 | このように書くと、おなじみ`cat`コマンドのように、「標準入力から文字列を読んで、標準出力にそのまま書き込む」処理ができます。 245 | 246 | おなじみ`main`の定義に書けば、簡易版`cat`コマンドのできあがりです! 247 | 248 | ```haskell 249 | main = do 250 | input <- getContents 251 | putStr input 252 | ``` 253 | 254 | ただし、**細い左矢印 `<-` は`do`の中でしか使えない**、という点に注意してください。 255 | 256 | 次のプログラムを実行しようとすると、エラーになってしまいます。 257 | 258 | ```haskell 259 | main = 260 | input <- getContents 261 | putStr input 262 | ``` 263 | 264 | ``` 265 | shell> stack exec runhaskell no-do.hs 266 | no-do.hs:2:9: error: 267 | parse error on input ‘<-’ 268 | Perhaps this statement should be within a 'do' block? 269 | | 270 | 2 | input <- getContents 271 | | ^^ 272 | ``` 273 | 274 | `Perhaps this statement should be within a 'do' block?`というエラーメッセージは、わざわざ「この命令(`input <- getContents`)は`do`ブロックの中で使うべきなのでは?」と提案してきてくれています。 275 | 「わかってるなら補ってくれてもいいのに...」という気もしますが、気を取り直して`do`を付け足しましょう。 276 | 277 | ## 課題の解き方 278 | 279 | `getContents`で取得した標準入力の文字列を、細い左矢印 `<-` で取り出し、適当な変数に代入してください。 280 | その代入した変数に対して、「改行文字で区切って、逆順に並び替え」る関数を順番に適用しましょう。 281 | 最後は逆順になったリストを改行文字で結合し直し、`putStr`で出力すればバッチリです。 282 | あっ、すべて`do`記法の中に書くのをお忘れなく。 283 | 284 | ## コラム: 「代入」か「束縛」か 285 | 286 | ところで、読者の皆さんの中には、「Haskellでは『代入 (assignment)』という言葉は使わないよ。『束縛 (binding)』だよ」という主張を聞いたことがあるかも知れません([例えばこれ][qiita])。 287 | `input <- getContents`という行は、「`input`という変数に`getContents`の結果を**束縛する**」というような説明は実際しばしばあります。 288 | しかしこの当入門「失敗しながら学ぶHaskell入門」では、「理論的な背景の違いはあるものの、少なくともソースコードを書いて実行するだけの我々からみれば、実用上の違いはほとんどない」という立場から、読者にとってより親しみの深いであろう「代入」という単語を敢えて使っています。 289 | この問題についての詳細は[「名前の束縛」という名の束縛][fumieval]という記事を、またその元となった議論が書かれている[こちら][issue28]や[こちら][pr35]、[Slackにおけるこのあたりの議論][slack1]と[その続き][slack2](JSONで読みにくいですがあしからず...)もご覧ください。 290 | 291 | [qiita]: https://qiita.com/lotz/items/aca1d179c14d4dca5099 "Haskellの変数には値を代入しません、束縛します。" 292 | [fumieval]: http://fumieval.hatenablog.com/entry/2018/10/31/150056 "「名前の束縛」という名の束縛" 293 | [issue28]: https://github.com/haskell-jp/makeMistakesToLearnHaskell/issues/28 "再代入ができないことや「束縛」という単語について触れる #28" 294 | [pr35]: https://github.com/haskell-jp/makeMistakesToLearnHaskell/pull/35 "Fix #28 代入と束縛の区別について追記 #35" 295 | [slack1]: https://github.com/haskell-jp/slack-log/blob/master/doc/json/CD87P78HF/3.json#L260-L302 296 | [slack2]: https://github.com/haskell-jp/slack-log/blob/master/doc/json/CD87P78HF/4.json#L1-L80 297 | -------------------------------------------------------------------------------- /assets/5.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | line1 <- getLine 3 | let principal = read line1 4 | 5 | line2 <- getLine 6 | let interestRate = read line2 7 | 8 | line3 <- getLine 9 | let years = read line3 10 | 11 | print (principal * (1 + interestRate / 100) ^ years) 12 | -------------------------------------------------------------------------------- /assets/5.md: -------------------------------------------------------------------------------- 1 | # 型注釈をつける・入力を数値に変換する 2 | 3 | 複利計算をして、将来儲かるお金を計算しましょう💰: 4 | 5 | 1. 標準入力から3行の入力を受け取ります。 6 | 1. 1行目は元金の額として解釈します。 7 | 1. 2行目は1年間あたりの金利(**単位はパーセント**)として解釈します。 8 | 1. 3行目は預けた年数として解釈します。 9 | 1. 以上の入力を元に、預けた年数後の預入額を計算してください。 10 | 11 | ## 注意事項 12 | 13 | - 小数点以下の値に対して、四捨五入などの丸め処理をする必要はありません。 14 | - 浮動小数点数の丸め誤差を気にする必要もありません。 15 | 16 | ## 実行結果例 17 | 18 | 「元金100円、金利1%で2年間預けた場合」を計算した結果です。 19 | 20 | ```bash 21 | shell> stack exec runhaskell 5.hs 22 | 100 # ここはユーザーが入力する箇所 23 | 1 # ここもユーザーが入力する箇所 24 | 2 # ここまでユーザーが入力する箇所 25 | 102.01 26 | ``` 27 | 28 | ## 必要な知識 29 | 30 | ### `getLine`で標準入力から1行ずつ読み出す 31 | 32 | 前回の課題では、`getContents`という命令を使って、標準入力からすべての文字列を読み出す方法を習いました。 33 | 今回の課題では、「標準入力から3行の入力を」受け取り、なおかつ各行で受け取った文字列を別々に扱う必要があります。 34 | 標準入力から1行ずつ読まなければならないでしょう。 35 | そのために使う関数は`getLine`といいます。 36 | 37 | 早速GHCiで試してみましょう。 38 | 39 | ```haskell 40 | ghci> getLine 41 | ``` 42 | 43 | `getLine`と入力してEnterキーやReturnキーを押した時点では、プロンプトが表示されず、GHCiが止まったように見えますよね。 44 | これは`getLine`関数が標準入力から入力を待ち受けている証拠です。 45 | そのまま適当な文字列を入力してEnterまたはReturnを押せば、入力した結果がGHCiによって出力されるはずです。 46 | 47 | ```haskell 48 | ghci> getLine 49 | This is a line I entered! 50 | -- ^ ここでEnterまたはReturn 51 | "This is a line I entered!" 52 | -- ^ これはGHCiが`getLine`を実行した結果を出力したもの 53 | ghci> 54 | ``` 55 | 56 | EnterやReturnを押した直後に出てきた文字列、ここでは`"This is a line I entered!"`に注目してください。 57 | これは`getLine`を実行した結果の文字列を、Haskellにおける文字列リテラルの形式で出力したものです。 58 | Rubyで言うところの`inspect`、Pythonで言うところの`repr()`を実行した結果だと言えば通じるでしょうか? 59 | 非印刷文字や改行文字など、普通に`putStrLn`や`putStr`で表示しようとするとわかりづらい文字も見やすくしてくれます。 60 | 61 | ### 入力を数値に変換する 62 | 63 | 続いて、`getLine`で受け取った文字列を数値に変換することで、足し算やかけ算などの計算に使えるようにしましょう。 64 | そのためには、`read`という関数を使います。 65 | 66 | ただし、この`read`関数、GHCiで扱う際は非常にやっかいな性質を持っています。 67 | そのまま試そうとしても、下記のようにエラーになってしまいます。 68 | 69 | ```haskell 70 | ghci> read "2.0" 71 | *** Exception: Prelude.read: no parse 72 | ``` 73 | 74 | これを避けるためにはいくつかの方法があります。まずは最も確実な方法をお伝えしましょう。 75 | 76 | #### 型推論とは 77 | 78 | ...と、その前に、重要な話なのでこのエラーの詳細について解説させてください。 79 | 少し難しい話ですがどうかお付き合いを。🙏 80 | 少なくとも今回の課題を解く**だけ**であれば必要ないので、難しい場合は次の節まで飛ばしてしまうのもありかもしれません。 81 | 82 | 先ほどのエラーは、Haskellの「型推論」という機能と、そのデフォルトの挙動が組み合わさってしまったことによるエラーです。 83 | 84 | さて、「型推論」とは一体なんでしょうか? 85 | 文字通り「型」を推論する機能なのですが、そもそもその「型」とは何か、といった点から説明したいと思います。 86 | Haskellやその他の多くのプログラミング言語において「型」とは、「文字」や「整数」、「文字列」といった、値の「種類」を表す分類です。 87 | Haskellは静的型付言語であるため、Haskellのソースコードに現れるありとあらゆる式は、プログラムとして実行する前の時点で、なんらかの「型」に分類されます。 88 | これは、これまでの課題で代入した、`main`関数や、`getContents`を実行した結果を結びつけた、変数も例外ではありません。 89 | 90 | 他の静的型付言語を経験した方であれば、`main`関数を定義した際や`getContents`を実行した結果を代入した際に、型注釈を書かなかったことにお気づきかもしれません。 91 | 実はHaskellは、静的型付言語でありながら、多くの箇所で型注釈を書かなくてもよいことになっています。 92 | それを可能にしてくれているのがその「型推論」という機能です。 93 | 94 | 「型推論」は、実際に関数や変数を含む式がどのように使われているかを見ることで、自動で式の型を決定してくれる機能です。 95 | これまでの課題では、私たちは文字列(例えば`"Hello, world!"`)や数値(例えば`60`や`1.7`)といった値について、それらが何の型であるか、特に言及せずに扱うことができました。 96 | 実際のところ、Haskellでは文字列は`String`という型に属しますし、`60`のような整数は通常`Integer`型に属し、`1.7`のような小数を含む数値は、通常`Double`型に属します。 97 | 加えて、`main`関数には、`IO`という変わった名前の型に必ず属します(これについてはもっと先の課題で紹介しましょう)。 98 | これまでそれらの名前に一切触れなくて済んだのは、型推論のおかげなのです。 99 | 100 | 型推論は、`read`関数のような、様々な型の値を返すことができる関数(「多相関数」と呼ばれます)を簡単に扱えるようにするために、とても重要な機能です。 101 | しかし、「様々な型の値を返すことができる関数」はその原理上、型推論にとってやっかいなものになることがあります。 102 | Haskellの強力な型推論機能を持ってしても、**どんな型の値を返すか決定できない場合がある**のです。 103 | 例えば先ほどのように、GHCiの中で`read`関数を使用した場合がその典型的なケースです。 104 | `read`関数の**返す値**はその性質上、「様々な型の値」になり得ます。 105 | そして、GHCiがHaskellの式を評価した結果を表示する関数 --- 第2章で学習した、`print`関数のことですね --- は、「様々な型の値」を**引数として受け取る**関数(これも「多相関数」と呼ばれます)となっています。 106 | このように、「様々な型の値」を返す関数と、それを引数として受け取る関数が組み合わさった式を見つけると、型推論機能は決定不能に陥ってしまいます。 107 | 108 | 通常であれば、そうした場合Haskellは決定できなかった式を「曖昧な(ambiguous)型」として扱い**型エラー**にします。 109 | しかし、それでは困る場合が(特に、GHCiのように短い式を評価することが多いプログラムでは)あるので、Haskellは一部のケースにおいて「曖昧な型」をエラーとしないで、代わりに「デフォルトの型」として解釈することにしています。 110 | それが今回エラーを起こした`read "2.0"`の結果の型なのです。 111 | GHCiは、「`read "2.0"`の結果を`print`関数で表示する式」を型推論する際、`print`関数に渡す引数の型を、そのデフォルトの型である`()`という変な名前の型として解釈します。「ユニット」と呼ばれています。 112 | この「ユニット」という型の値を`read`関数によって(無事、エラーを起こさず)返させるには、`"()"`という文字列を与えればよいのです。 113 | 114 | ``` 115 | ghci> read "()" 116 | () 117 | ``` 118 | 119 | `*** Exception`などと出ていませんね。これは、ちゃんとエラーを起こさずに`read "()"`を実行できた証拠です!よかった!😌 120 | 121 | #### 確実な解決方法: 型注釈をつける 122 | 123 | ... いや、`read "()"`をエラーを起こさずに実行できてもあまりうれしいことはありません。 124 | 私たちがエラーを起こさずに実行したいのは`read "2.0"`の方です。 125 | `read "2.0"`という式の型がデフォルトの型ではなく、希望する数値の型(`Double`型)とするには、どうすればいいでしょうか? 126 | それを解決するのが型注釈です。 127 | 128 | C言語などの一部の静的型付のプログラミング言語では、変数や関数を定義する際に型宣言をすることができます。 129 | Haskellでは、同じように変数や関数の型宣言をする機能に加え、**あらゆる式**に対して「ここの式はこの型ですよ」と明確に示す機能があります。 130 | それが型注釈です。 131 | 132 | 具体的には、「ここの式はこの型ですよ」とHaskellに教えてあげたい式の後ろに、`:: 型の名前`と書けば、指定した式に対して型注釈を加えることができます。 133 | 下記は`read "2.0"`という式に対して数値の型(`Double`型)という型注釈を加える例です: 134 | 135 | ``` 136 | ghci> read "2.0" :: Double 137 | 2.0 138 | ``` 139 | 140 | ついに、`read "2.0"`をエラーを起こすことなく実行できました!🎉 141 | 142 | 型注釈の使い方についてもう少し捕捉しましょう。 143 | 他の式と同じように、カッコで囲うことで、どの式に注釈を加えたいかをコントロールすることもできます。 144 | 次に示す例では、カッコで囲った、`(read "2" + read "1")`という式全体に対して整数型(`Integer`)という型注釈を加えています。 145 | 146 | ``` 147 | ghci> (read "2" + read "1") :: Integer 148 | 3 149 | ``` 150 | 151 | もちろん、間違った型注釈をすると、ちゃんと型エラーにしてくれます。 152 | 153 | ``` 154 | ghci> "2.0" :: Double 155 | 156 | :2:1: error: 157 | • Couldn't match expected type ‘Double’ with actual type ‘[Char]’ 158 | • In the expression: "2.0" :: Double 159 | In an equation for ‘it’: it = "2.0" :: Double 160 | ``` 161 | 162 | これまでにも見かけたことがあるかも知れませんが、複数行にまたがる`Couldn't match expected type ... ` や、`No instance for ...`などというエラーメッセージを見たら、型エラーなんだと考えてください。`read "2.0"`を実行したとき出た`*** Exception: ...`とは異なりますよね。 163 | 164 | #### よくある解決方法: 型が曖昧にならないように使用する 165 | 166 | 型注釈を適切に使うことで、`read`関数が文字列をおかしな型の値に変換するのを回避できることを学びました。 167 | 一方この問題は、もう少し大きなHaskellのプログラムを書く場合、問題にならないことが多いです。 168 | 少なくとも今回の課題を解くだけであれば、気にしなくて良いはずです。 169 | 型推論機能がちゃんと働いて、どの型なのかを曖昧にならずに判断できる式を書けばよいのです。それだけです。 170 | 例えば今回の`read "2.0"`であっても、次のように`+ 0.0` と着けるだけでエラーを回避できます。 171 | 172 | ```haskell 173 | ghci> read "2.0" + 0.0 174 | 2.0 175 | ``` 176 | 177 | これは、`read "2.0"`の結果に対して`+ 0.0`という計算をしようとしていることから、Haskellの型推論機能が「あっ、この`read "2.0"`の結果は数値の型(`Double`型)として解釈すればいいんだ!」と判断しているためです。 178 | 179 | ### 複利計算の公式 180 | 181 | それでは肝心の、「預けた年数後の預入額」を計算する際の式をHaskellで書いてみましょう。 182 | 日本語で言うと、次のような式で計算されます。 183 | 184 | ``` 185 | 預けた年数後の預入額 = 元金 × (1 + 年利)の「年数」乗 186 | ``` 187 | 188 | おっと、「『年数』乗」ということは、累乗の計算が必要ですね。 189 | ほかのプログラミング言語で累乗を計算する際は`**`や`^`という演算子、はたまた`pow`関数などを使用しますが、Haskellでは果たして何を使うのでしょうか!? 190 | 191 | ... 答えはキャレット `^` です! 192 | 193 | ```haskell 194 | ghci> 2 ^ 10 195 | 1024 196 | ``` 197 | 198 | そして、ここで言う「年利」は問題の仕様上、パーセントとして入力されるため、100で割る必要があります。 199 | つまり次のような式になりますね。 200 | 201 | ``` 202 | 預けた年数後の預入額 = 元金 × (1 + 年利(%) ÷ 100) ^ 年数 203 | ``` 204 | 205 | 第2章で習ったとおり、足し算・かけ算・割り算にはそれぞれ `+`, `*`, `/` を使います。 206 | 元金・年利(%)・預けた年数をそれぞれ`principal`・`interestRate`・`years`という変数に割り当てれば、最終的に下記のようなHaskellの式になるでしょう: 207 | 208 | ```haskell 209 | principal * (1 + interestRate / 100) ^ years 210 | ``` 211 | 212 | ### `let`で「純粋な計算」の結果を変数に代入する 213 | 214 | 今回の課題は、ここまでで説明した知識で十分解くことができます。 215 | しかし、もう一つだけ新しい構文を覚えることで、もっとエレガントに解けるようになります。それは`let`です。 216 | `let`を使うと、`lines`関数や`reverse`関数、それから足し算 `+`やかけ算 `*`などによって作られる値を、変数に代入できるようになります。 217 | 例えば課題4では、次のように「文字列を、改行文字で区切って、逆順に並び替えて標準出力に出力する」までの処理を、一つの大きな式で書いていましたね。 218 | 219 | ```haskell 220 | putStr (unlines (reverse (lines input))) 221 | ``` 222 | 223 | 課題4を解いていた方の中には、次のように、「改行文字で区切った結果」や「逆順に並び替えた結果」などを逐一変数に代入したくなった方もいらっしゃるかも知れません。 224 | 225 | ```haskell 226 | splitByLine = lines input 227 | reversed = reverse splitByLine 228 | result = unlines reversed 229 | putStr result 230 | ``` 231 | 232 | 残念ながら、Haskellではこれはできません(実は[できるようにしようという議論][1]もありますが、解釈が曖昧になるケースがあり、2018年12月現在保留されています)。 233 | `do`記法の中で(`getContents`のような、「命令」以外によって作られる)普通の値を変数に代入する場合は、次のように書きましょう。 234 | 課題4の解答を`let`で書き換えた場合を例に示します。 235 | 236 | [1]: https://github.com/ghc-proposals/ghc-proposals/pull/62 237 | 238 | ```haskell 239 | do 240 | -- ... 省略 241 | let splitByLine = lines input 242 | let reversed = reverse splitByLine 243 | let result = unlines reversed 244 | -- ... 省略 245 | ``` 246 | 247 | `let <変数名> = <式>`と書くことで`<変数名>`の変数に`<式>`の結果を代入することができます。 248 | `do`の中で`let`で代入する変数がいくつも続く場合、`let`は1行目だけにして、2行目以降は省略することができます。 249 | 先ほどの例で言うと、次のように書き換えることができる、ということです。 250 | 251 | ```haskell 252 | do 253 | -- ... 省略 254 | let splitByLine = lines input 255 | reversed = reverse splitByLine 256 | result = unlines reversed 257 | -- ... 省略 258 | ``` 259 | 260 | 間に「命令」が挟まってしまった場合、この方法は使えません。 261 | 262 | ```haskell 263 | do 264 | -- ... 省略 265 | let splitByLine = lines input 266 | anotherInput <- getContents 267 | reversed = reverse splitByLine -- これはダメ! 268 | result = unlines reversed -- これもダメ! 269 | -- ... 省略 270 | ``` 271 | 272 | その場合、改めて`let`を挟みましょう。 273 | 274 | ```haskell 275 | do 276 | -- ... 省略 277 | let splitByLine = lines input 278 | anotherInput <- getContents 279 | let reversed = reverse splitByLine -- これはOK! 280 | result = unlines reversed -- これもOK! 281 | -- ... 省略 282 | ``` 283 | 284 | そして、ここでもう一度強調しておきたいのは、`let`で代入するのは、`getContents`のような、「命令」以外によって作られる普通の値だけ、という点です。 285 | Haskellでは、`getContents`や`putStrLn`のような入出力などを行う際必要な「命令」と、それ以外の「純粋な計算」とを厳密に区別しています。 286 | 「純粋な計算」とはなんでしょうか?それは、次のような特徴を持った計算です。 287 | 288 | 1. 「副作用」を持たない: 289 | - 「純粋な計算」は**計算以外の処理を一切行いません**。 290 | 例えば、`1 + 2 * 7`という「純粋な計算」は、計算を始めてから`15`という結果を返すまでの間に、どこかのファイルに何かを書き込んだり、外部のサーバーと何かしらおしゃべりしたり、ましてやミサイルを発射したり、なんてことは決してしません。`lines`関数や`reverse`関数も同様です。 291 | あくまでも、引数や両辺に渡された値を最終的な結果に変換するだけのことしかしないのです。 292 | 1. 同じ式は必ず同じ結果になる: 293 | - 「純粋な計算」の結果(関数や演算子の戻り値)は、**同じ関数や演算子に同じ値を同じ順番で渡した場合、必ず同じ値**となります。 294 | 例えば、`getContents`関数はユーザーなどからの入力を結果として返すため、実行する度に違う結果を返す可能性があります。ユーザーは気まぐれなのです。 295 | それからまだ紹介してませんが、乱数を取得する関数も、実行する度に結果が変わるので「純粋な計算」とはいえません。乱数もやっぱり気まぐれなのです。 296 | 297 | 結果的に「純粋な計算」は、**外から受けうる影響の範囲と、外に与えうる影響の範囲が、常に明瞭**となります。 298 | 「純粋な計算」は、関数や演算子に渡した値からしか影響を受けませんし、`let`で代入する変数以外に影響を与えることもありません。 299 | このような性質があるからこそ、Haskellでは「純粋な計算」とそれ以外の「命令」とを厳密に区別しています。 300 | 区別することで、あなたが書いたソースコードの「影響範囲が明瞭な箇所」と「それ以外の箇所」を分離し、理解しやすくします(実際には、「それ以外の箇所」だらけのソースコードになってしまうことも多いですが😅)。 301 | 302 | ここまでを見る限り、代入の際に使う構文ぐらいしか違いがありません。 303 | しかしこの先で、この使い分けの裏に隠された巧妙なトリックを知ることになるでしょう。乞うご期待。 304 | 305 | まとめて、以下の点を押さえておきましょう。 306 | 307 | - `let <変数名> = <式>` 308 | - 代入する値の種類: 「純粋な計算」によって作られる値 309 | - 例: `let splitByLine = lines input` 310 | - `<変数名> <- <命令>` 311 | - 代入する値の種類: 「命令」を実行した結果得られる値 312 | - 例: `input <- getContents` 313 | 314 | また、`<-`と同様、`do`記法の中でしかこの書き方はできません。`do`記法の中以外で代入する方法については、またの機会に。 315 | 316 | #### `let`で「文字列を数値に変換する式」と「複利計算を行う式」を明確に分ける 317 | 318 | 今回の課題で`let`を使うならば、`read`関数で読み出した文字列を数値に変換する処理と、実際に複利計算を行う処理を分けるために使うのが良いでしょう。 319 | 「数値を計算する」というこのアプリケーションのコアに当たる処理と、(標準入力という)外の世界からやってくる文字列をコアの処理向けに変換する処理を分割できれば、コアの処理の再利用性を高められるでしょうし、読みやすくもなりそうです。 320 | 321 | 課題のヒントを兼ねて、「1行目を`getLine`で受け取って、元金の額として変換する」処理までを例示しましょう。 322 | 323 | ```haskell 324 | line1 <- getLine 325 | let principal = read line1 326 | ``` 327 | 328 | これと同じ要領で他の行を変換した数値も`let`で代入すれば、「`principal * (1 + interestRate / 100) ^ years`」という式にすっきり当てはめることができますね! 329 | `let`を使わないバージョンと両方書いてみて、比べてみるのも良いでしょう。 330 | 331 | ## 課題の解き方 332 | 333 | 今回も課題4と同様、`do`記法の中で`<-`を使って`getLine`関数の実行結果を代入するのがポイントです。 334 | `getLine`で読んだ各行を変数に代入した後は、`read`関数で数値に変換しつつ、上記の複利計算の式に当てはめましょう。`read`関数を実行した結果を`let`で新しい変数に代入すると、よりわかりやすく書けます。 335 | 「Haskellでは関数呼び出しが最も優先して結合される」というルールを思い出しつつ、カッコで囲う場所にはくれぐれも気をつけてください。 336 | -------------------------------------------------------------------------------- /assets/exercise.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px auto 0px auto; 3 | background-color: #f8f8f8; 4 | width: 700px; 5 | } 6 | 7 | p,ol,ul,footer { 8 | line-height: 1.5em; 9 | } 10 | 11 | a:link { 12 | color: #c11; 13 | text-decoration: none; 14 | } 15 | 16 | a:visited { 17 | color: #a11; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | #container { 25 | border-radius: 8px; 26 | background-color: #fff; 27 | padding: 0 5px 5px; 28 | } 29 | 30 | h1 { 31 | font-size: 150%; 32 | border-left: solid 5px #c11; 33 | padding-left: 3px; 34 | } 35 | 36 | h2 { 37 | margin-top: 1.5em; 38 | font-size: 130%; 39 | border-left: solid 3px #c11; 40 | padding-left: 3px; 41 | } 42 | 43 | h3 { 44 | font-size: 115%; 45 | border-left: solid 2px #c11; 46 | padding-left: 3px; 47 | } 48 | 49 | ol,ul { 50 | padding-left: 1em; 51 | padding-bottom: 0.5em; 52 | } 53 | 54 | li { 55 | margin-top: 0.2em; 56 | } 57 | 58 | pre, code { 59 | white-space: pre; 60 | font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif; 61 | } 62 | 63 | pre { 64 | margin: 1em; 65 | white-space: pre-wrap; 66 | color: #000000; 67 | overflow: auto; 68 | border: ridge; 69 | border-top-left-radius : 5px; 70 | border-top-right-radius : 5px; 71 | -webkit-border-top-left-radius : 5px; 72 | -webkit-border-top-right-radius : 5px; 73 | border-bottom-left-radius : 5px; 74 | border-bottom-right-radius : 5px; 75 | -webkit-border-bottom-left-radius : 5px; 76 | -webkit-border-bottom-right-radius: 5px; 77 | padding : 8px; 78 | } 79 | -------------------------------------------------------------------------------- /assets/goal.hs: -------------------------------------------------------------------------------- 1 | #!/bin/env stack 2 | {- 3 | stack script --resolver=lts-11.13 4 | --package split 5 | --package containers 6 | -} 7 | import Data.List.Split 8 | import Data.Map.Strict (Map) 9 | import qualified Data.Map.Strict as Map 10 | 11 | import Data.Traversable (for) 12 | import System.Environment (getArgs) 13 | import System.IO (BufferMode(..), hSetBuffering, stdout) 14 | 15 | type Entry = (String, Int) 16 | 17 | main :: IO () 18 | main = do 19 | files <- getArgs 20 | entries <- 21 | for files (\path -> do 22 | contents <- readFile path 23 | return (parseEntries contents) 24 | ) 25 | let summary = Map.fromListWith (\x y -> x + y) (concat entries) 26 | putStr (formatSummry summary) 27 | 28 | parseEntries :: String -> [Entry] 29 | parseEntries s = map parseEntry (lines s) 30 | 31 | parseEntry :: String -> Entry 32 | parseEntry s = 33 | case splitOn "\t" s of 34 | [c, sv] -> (c, read sv) 35 | _ -> error ("Invalid entry: " ++ show s) 36 | 37 | formatSummry :: Map String Int -> String 38 | formatSummry summary = unlines (map formatSummaryItem (Map.toList summary)) 39 | 40 | formatSummaryItem :: (String, Int) -> String 41 | formatSummaryItem (cat, total) = cat ++ "\t" ++ show total 42 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: makeMistakesToLearnHaskell 2 | version: '0.1.0.0' 3 | synopsis: Make mistakes to learn Haskell! 4 | category: Education 5 | author: Yuji Yamamoto 6 | maintainer: whosekiteneverfly@gmail.com 7 | copyright: 2017 Yuji Yamamoto 8 | license: Apache-2.0 9 | homepage: https://github.com/haskell-jp/makeMistakesToLearnHaskell#readme 10 | git: https://github.com/haskell-jp/makeMistakesToLearnHaskell.git 11 | data-files: 12 | - assets/*.md 13 | - assets/*.hs 14 | - assets/exercise.css 15 | extra-source-files: 16 | - README.md 17 | - src/imports/*.hs 18 | - src/test/imports/*.hs 19 | # test data here. see https://stackoverflow.com/questions/29361413/including-data-files-only-in-cabal-test-suites 20 | - test/assets/1/*.hs 21 | - test/assets/common/*.hs 22 | 23 | library: 24 | source-dirs: src 25 | default-extensions: 26 | - CPP 27 | - DeriveGeneric 28 | - DeriveDataTypeable 29 | - OverloadedStrings 30 | include-dirs: 31 | - src 32 | exposed-modules: 33 | - Education.MakeMistakesToLearnHaskell 34 | - Education.MakeMistakesToLearnHaskell.Env 35 | - Education.MakeMistakesToLearnHaskell.Evaluator.Types 36 | - Education.MakeMistakesToLearnHaskell.Exercise 37 | - Education.MakeMistakesToLearnHaskell.Exercise.Types 38 | # ghc-options: -Wno-unused-imports 39 | # Why doesn't -Wno-unused-imports work? 40 | dependencies: 41 | - base >=4.7 && <5 42 | # TODO: Decide version 43 | - ansi-terminal 44 | - QuickCheck 45 | - bytestring 46 | - cmark 47 | - directory 48 | - errors 49 | - filepath 50 | - ghc-syntax-highlighter 51 | # - i18n Use in the future 52 | - open-browser 53 | - optparse-applicative 54 | - QuickCheck 55 | - regex-applicative 56 | - safe 57 | - text 58 | - transformers 59 | - typed-process 60 | when: 61 | - condition: os(windows) 62 | dependencies: 63 | - Win32 64 | 65 | executables: 66 | mmlh: 67 | main: Main.hs 68 | source-dirs: app 69 | ghc-options: 70 | - -threaded 71 | - -rtsopts 72 | - -with-rtsopts=-N 73 | dependencies: 74 | - base 75 | - makeMistakesToLearnHaskell 76 | 77 | tests: 78 | makeMistakesToLearnHaskell-test: 79 | main: Spec.hs 80 | source-dirs: test 81 | default-extensions: 82 | - CPP 83 | - OverloadedStrings 84 | ghc-options: 85 | - -threaded 86 | - -rtsopts 87 | - -with-rtsopts=-N 88 | dependencies: 89 | - base 90 | - makeMistakesToLearnHaskell 91 | - bytestring 92 | - directory 93 | - filepath 94 | - hspec 95 | - hspec-core 96 | - main-tester >=0.2 97 | - QuickCheck 98 | - text 99 | -------------------------------------------------------------------------------- /scripts/gen-error-messages.hs: -------------------------------------------------------------------------------- 1 | import Control.Monad (forM_, void) 2 | import qualified Data.ByteString as ByteString 3 | import Data.Function ((&)) 4 | import qualified System.Directory as Dir 5 | import qualified System.Environment as Env 6 | import qualified System.FilePath as Path 7 | import System.FilePath (()) 8 | import qualified System.IO as IO 9 | import qualified System.Process.Typed as Process 10 | 11 | 12 | main :: IO () 13 | main = do 14 | files <- Env.getArgs 15 | forM_ files $ \file -> do 16 | let outDir = Path.takeDirectory file "error-messages" 17 | Dir.createDirectoryIfMissing True outDir 18 | 19 | IO.withFile (outDir Path.takeBaseName file ++ ".txt") IO.WriteMode $ \hd -> do 20 | let p = 21 | Process.proc "stack" ["exec", "ghc", file] 22 | & Process.setStderr (Process.useHandleClose hd) 23 | & Process.setStdout (Process.useHandleClose hd) 24 | void $ Process.runProcess p 25 | -------------------------------------------------------------------------------- /scripts/gen-test-cases.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | exercise_name="$1" 6 | shift 7 | 8 | case_files="$@" 9 | 10 | for case_file in ${case_files[@]} ; do 11 | case_name="$(basename "$case_file" | cut -f 1 -d '.')" 12 | cat < 9 | 10 | import Education.MakeMistakesToLearnHaskell.Env 11 | import qualified Education.MakeMistakesToLearnHaskell.Exercise as Exercise 12 | import qualified Education.MakeMistakesToLearnHaskell.Evaluator.RunHaskell as RunHaskell 13 | import Education.MakeMistakesToLearnHaskell.Error 14 | import Education.MakeMistakesToLearnHaskell.Text 15 | 16 | import qualified Options.Applicative as Opt 17 | import System.Console.ANSI 18 | 19 | main :: IO () 20 | main = do 21 | avoidCodingError 22 | args <- Env.getArgs 23 | if null args then 24 | printExerciseList 25 | else do 26 | cmd <- Opt.execParser (Opt.info (cmdParser <**> Opt.helper) Opt.idm) 27 | withMainEnv $ \e -> 28 | case cmd of 29 | Show isTerminal n -> showExercise e isTerminal [n] 30 | Verify path -> verifySource e [path] 31 | 32 | 33 | withMainEnv :: (Env -> IO r) -> IO r 34 | withMainEnv doAction = do 35 | d <- Env.getEnv homePathEnvVarName <|> Dir.getXdgDirectory Dir.XdgData appName 36 | Dir.createDirectoryIfMissing True d 37 | IO.withFile (d "debug.log") IO.WriteMode $ \h -> do 38 | let e = defaultEnv 39 | { logDebug = ByteString.hPutStr h . (<> "\n") 40 | , appHomePath = d 41 | , runHaskell = RunHaskell.runFile e 42 | } 43 | doAction e 44 | 45 | data Cmd 46 | = Show Bool String 47 | | Verify FilePath 48 | deriving (Eq, Show) 49 | 50 | optTerminalP :: Opt.Parser Bool 51 | optTerminalP = Opt.switch $ Opt.long "terminal" <> Opt.help "display to terminal" 52 | 53 | showCmdP :: Opt.Parser Cmd 54 | showCmdP = Show <$> optTerminalP 55 | <*> Opt.argument Opt.str (Opt.metavar "") 56 | 57 | verifyCmdP :: Opt.Parser Cmd 58 | verifyCmdP = Verify <$> Opt.argument Opt.str (Opt.metavar "") 59 | 60 | cmdParser :: Opt.Parser Cmd 61 | cmdParser = Opt.hsubparser 62 | $ Opt.command "show" (Opt.info showCmdP (Opt.progDesc "Show Exercise")) 63 | <> Opt.command "verify" (Opt.info verifyCmdP (Opt.progDesc "Verify Exercise")) 64 | 65 | 66 | printExerciseList :: IO () 67 | printExerciseList = do 68 | Text.putStrLn "# Make Mistakes to Learn Haskell!" 69 | Text.putStrLn "" 70 | Text.putStrLn "## Contents" 71 | 72 | let printHeader h = Text.putStrLn $ "- " <> h 73 | mapM_ printHeader =<< Exercise.loadHeaders 74 | 75 | Text.putStrLn $ "\nRun `" <> Text.pack appName <> " show ` to try the exercise." 76 | 77 | 78 | verifySource :: Env -> [FilePath] -> IO () 79 | verifySource _ [] = die "Specify the Haskell source file to veirfy!" 80 | verifySource e (file : _) = do 81 | currentExercise <- Exercise.loadLastShown e 82 | result <- Exercise.verify currentExercise e file 83 | case result of 84 | Exercise.Success details -> do 85 | Text.putStrLn details 86 | withSGR [SetColor Foreground Vivid Green] $ 87 | putStrLn "\n\nSUCCESS: Congratulations! Your solution got compiled and ran correctly!" 88 | 89 | showExampleSolution currentExercise 90 | Exit.exitSuccess 91 | 92 | Exercise.Fail details -> do 93 | Text.putStrLn details 94 | withSGR [SetColor Foreground Vivid Red] $ 95 | putStrLn "\nFAIL: Your solution didn't pass. Try again!" 96 | putStrLn $ "HINT: Verified the exercise " ++ Exercise.name currentExercise ++ ". Note I verify the last `mmlh show`-ed exercise.\n" 97 | Exit.exitFailure 98 | 99 | Exercise.Error details -> do 100 | Error.errLn $ Text.toStrict $ details <> "\n\n" 101 | die "An unexpected error occurred when evaluating your solution." 102 | 103 | Exercise.NotVerified -> do 104 | putStrLn $ "[NOT VERIFIED] the exercise " ++ Exercise.name currentExercise ++ " has no test. Go ahead!" 105 | Exit.exitSuccess 106 | 107 | Exercise.NotYetImplemented -> do 108 | putStrLn 109 | $ "[NOT YET IMPLEMENTED] Sorry, the test of exercise " 110 | ++ Exercise.name currentExercise 111 | ++ " is not yet implemented. Check by yourself!" 112 | 113 | showExampleSolution currentExercise 114 | Exit.exitSuccess 115 | where 116 | withSGR sgrs act = bracket_ (setSGR sgrs) (setSGR [Reset]) act 117 | 118 | showExampleSolution ex = do 119 | putStrLn $ "Here's an example solution of the exercise " ++ Exercise.name ex ++ ":\n" 120 | Text.putStr =<< Exercise.loadExampleSolution ex 121 | 122 | 123 | showExercise :: Env -> Bool -> [String] -> IO () 124 | showExercise _ _ [] = die "Specify an exercise number to show" 125 | showExercise env isTerminal (n : _) = do 126 | d <- Exercise.loadDescriptionByName n 127 | >>= dieWhenNothing ("Exercise id " ++ n ++ " not found!") 128 | Exercise.saveLastShownName env n 129 | showMarkdown d isTerminal n 130 | 131 | showMarkdown :: Text -> Bool -> String -> IO () 132 | showMarkdown md isTerminal n = do 133 | cssPath <- ("file://" <>) . TextS.pack <$> Paths.getDataFileName "assets/exercise.css" 134 | let htmlBody = CMark.commonmarkToHtml [CMark.optSafe] $ Text.toStrict md 135 | htmlHead = TextS.unlines 136 | [ "" 137 | , "" 138 | , "" 139 | , "" 140 | , " cssPath <> "\" />" 141 | , "" 142 | , "" 143 | , "
" 144 | ] 145 | htmlFoot = TextS.unlines 146 | [ "
" 147 | , "" 148 | , "" 149 | ] 150 | 151 | mkHtmlPath dir = dir <> "/" <> "mmlh-ex" <> n <> ".html" 152 | path <- mkHtmlPath <$> Dir.getTemporaryDirectory 153 | 154 | writeUtf8FileS path (htmlHead <> htmlBody <> htmlFoot) 155 | 156 | browserLaunched <- 157 | if not isTerminal then 158 | Browser.openBrowser path 159 | else 160 | return False 161 | 162 | unless browserLaunched $ 163 | Text.putStr $ removeAllTrailingSpace md 164 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Diagnosis.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Diagnosis 4 | ( appendDiagnosis 5 | ) where 6 | 7 | 8 | #include 9 | 10 | import Education.MakeMistakesToLearnHaskell.Text 11 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 12 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 13 | 14 | 15 | appendDiagnosis :: Diagnosis -> SourceCode -> ErrorMessage -> Details 16 | appendDiagnosis diagFunc srcCode errMsg = Text.intercalate "\n" $ 17 | [ errMsg' 18 | , "==================== mmlh HINT output ====================" 19 | , "" 20 | , diagFunc srcCode errMsg' 21 | ] 22 | where 23 | errMsg' = decodeUtf8 errMsg -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Env.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Env 4 | ( Env (..) 5 | , defaultEnv 6 | , RunHaskellParameters(runHaskellParametersArgs, runHaskellParametersStdin) 7 | , defaultRunHaskellParameters 8 | , appName 9 | , homePathEnvVarName 10 | , avoidCodingError 11 | ) 12 | where 13 | 14 | #include 15 | 16 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 17 | 18 | data RunHaskellParameters = RunHaskellParameters 19 | { runHaskellParametersArgs :: ![String] 20 | , runHaskellParametersStdin :: !ByteString 21 | } 22 | 23 | defaultRunHaskellParameters :: RunHaskellParameters 24 | defaultRunHaskellParameters = RunHaskellParameters [] "" 25 | 26 | data Env = Env 27 | { logDebug :: ByteString -> IO () 28 | , appHomePath :: FilePath 29 | , runHaskell :: RunHaskellParameters -> IO (Either RunHaskellError (ByteString, ByteString)) 30 | , envQcMaxSuccessSize :: Int 31 | } 32 | 33 | defaultEnv :: Env 34 | defaultEnv = Env 35 | { logDebug = error "Set logDebug to defaultEnv" 36 | , appHomePath = error "Set appHomePath to defaultEnv" 37 | , runHaskell = error "Set runHaskell to defaultEnv" 38 | , envQcMaxSuccessSize = 20 39 | } 40 | 41 | appName :: String 42 | appName = "mmlh" 43 | 44 | 45 | homePathEnvVarName :: String 46 | homePathEnvVarName = "MAKE_MISTAKES_TO_LEARN_HASKELL_HOME" 47 | 48 | avoidCodingError :: IO () 49 | #ifdef mingw32_HOST_OS 50 | avoidCodingError = 51 | IO.hSetEncoding IO.stdout $ mkLocaleEncoding TransliterateCodingFailure 52 | #else 53 | avoidCodingError = return () 54 | #endif 55 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Error.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Error where 4 | 5 | 6 | #include 7 | 8 | 9 | import Education.MakeMistakesToLearnHaskell.Env (appName) 10 | 11 | 12 | dieWhenNothing :: String -> Maybe a -> IO a 13 | dieWhenNothing _ (Just x) = return x 14 | dieWhenNothing msg _ = die msg 15 | 16 | 17 | die :: String -> IO a 18 | die msg = Exit.die $ appName ++ ": ERROR: " ++ msg 19 | 20 | 21 | throwWhenLeft :: Exception e => Either e a -> IO a 22 | throwWhenLeft = either throwIO return 23 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Evaluator/Regex.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | -- | Thin wrapper around @Text.Regex.Applicative@ 4 | -- useful for evaluating source code input by the user. 5 | 6 | module Education.MakeMistakesToLearnHaskell.Evaluator.Regex 7 | ( GhcToken 8 | , matchSub 9 | , matchesSub 10 | , dropUntilFirst 11 | 12 | , singleArgFunApp 13 | ) where 14 | 15 | 16 | #include 17 | 18 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 19 | 20 | type GhcToken = (GHC.Token, TextS.Text) 21 | 22 | 23 | matchSub :: Regex.RE s a -> [s] -> Maybe a 24 | matchSub re = 25 | Regex.match (Regex.few Regex.anySym *> re <* Regex.few Regex.anySym) 26 | 27 | 28 | matchesSub :: Regex.RE s a -> [s] -> Bool 29 | matchesSub re = isJust . matchSub re 30 | 31 | 32 | dropUntilFirst :: Regex.RE s a -> [s] -> [s] 33 | dropUntilFirst re xs = 34 | maybe xs (\(_before, _matched, after) -> after) $ Regex.findFirstInfix re xs 35 | 36 | 37 | -- NOTE: Depth seeems required due to the limitation of regex-applicative, 38 | -- which has to scan all regex before executing. 39 | singleArgFunApp :: Natural -> Regex.RE GhcToken SingleArgFunApp 40 | singleArgFunApp depth = 41 | insideParens 42 | $ SingleArgFunApp 43 | <$> identifier 44 | <* skipSpace 45 | <*> (if depth == 0 then pure Nothing else optional $ singleArgFunApp $ depth - 1) 46 | 47 | where 48 | symbol :: TextS.Text -> Regex.RE GhcToken () 49 | symbol t = void $ Regex.sym (GHC.SymbolTok, t) 50 | 51 | hasSympol :: TextS.Text -> Regex.RE GhcToken Bool 52 | hasSympol t = (symbol t $> True) <|> pure False 53 | 54 | identifier :: Regex.RE GhcToken TextS.Text 55 | identifier = snd <$> Regex.psym ((== GHC.VariableTok) . fst) 56 | 57 | skipSpace :: Regex.RE GhcToken () 58 | skipSpace = void $ optional (Regex.psym ((== GHC.SpaceTok) . fst)) 59 | 60 | insideParens :: Regex.RE GhcToken (HasParens -> a) -> Regex.RE GhcToken a 61 | insideParens re = 62 | (symbol "(" *> skipSpace *> re <*> (toHasParens <$> (skipSpace *> hasSympol ")"))) <|> (re <*> pure NoParens) 63 | 64 | toHasParens :: Bool -> HasParens 65 | toHasParens = bool OnlyOpenParen BothParens 66 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Evaluator/RunHaskell.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Evaluator.RunHaskell 4 | ( runFile 5 | , RunHaskellError(..) 6 | ) where 7 | 8 | 9 | #include 10 | 11 | 12 | import Education.MakeMistakesToLearnHaskell.Env 13 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 14 | 15 | 16 | runFile :: Env -> RunHaskellParameters -> IO (Either RunHaskellError (ByteString, ByteString)) 17 | runFile e rhp = do 18 | cmd <- resolveInterpreter 19 | case cmd of 20 | [] -> return $ Left RunHaskellNotFound 21 | (h:left) -> do 22 | let prc = 23 | Process.setStdin (Process.byteStringInput $ runHaskellParametersStdin rhp) 24 | $ Process.proc h 25 | $ left ++ runHaskellParametersArgs rhp 26 | (ecode, out, err) <- fixingCodePage e $ readProcess prc 27 | return $ case ecode of 28 | ExitSuccess -> Right (out, err) 29 | ExitFailure i -> Left $ RunHaskellFailure i err 30 | 31 | 32 | resolveInterpreter :: IO [String] 33 | resolveInterpreter = do 34 | stack <- Dir.findExecutable "stack" 35 | case stack of 36 | Just p -> return $ [p, "exec", executableName, "--"] ++ optionsAlwaysColor 37 | _ -> maybe [] (: optionsAlwaysColor) <$> Dir.findExecutable executableName 38 | 39 | 40 | optionsAlwaysColor :: [String] 41 | #ifdef mingw32_HOST_OS 42 | -- FIXME: Command Prompt can't handle -fdiagnostics-color=always properly. 43 | optionsAlwaysColor = [] 44 | #else 45 | optionsAlwaysColor = ["--ghc-arg=-fdiagnostics-color=always"] 46 | #endif 47 | 48 | 49 | executableName :: String 50 | executableName = "runhaskell" 51 | 52 | -- | Ref: https://github.com/commercialhaskell/stack/blob/a9042ad6fa1d7c813a1c79713a518ee521da9add/src/Stack/Build.hs#L306-L332 53 | fixingCodePage :: Env -> IO a -> IO a 54 | #ifdef mingw32_HOST_OS 55 | fixingCodePage e action = do 56 | cpInSave <- Win32.getConsoleCP 57 | cpOutSave <- Win32.getConsoleOutputCP 58 | -- TODO: delete to independent from Env 59 | logDebug e $ "Fixing ConsoleCP from " <> ByteString.pack (show cpInSave) 60 | logDebug e $ "Fixing ConsoleOutputCP from " <> ByteString.pack (show cpOutSave) 61 | 62 | let utf8 = 65001 63 | setInput = cpInSave /= utf8 64 | setOutput = cpOutSave /= utf8 65 | fixingInput 66 | | setInput = bracket_ 67 | (Win32.setConsoleCP utf8) 68 | (Win32.setConsoleCP cpInSave) 69 | | otherwise = id 70 | fixingOutput 71 | | setOutput = bracket_ 72 | (Win32.setConsoleOutputCP utf8) 73 | (Win32.setConsoleOutputCP cpOutSave) 74 | | otherwise = id 75 | fixingInput $ fixingOutput action 76 | #else 77 | fixingCodePage _ = id 78 | #endif 79 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Evaluator/Types.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Evaluator.Types 4 | ( ErrorCode 5 | , ErrorMessage 6 | , RunHaskellError(..) 7 | , SingleArgFunApp(..) 8 | , HasParens(..) 9 | ) where 10 | 11 | 12 | #include 13 | 14 | 15 | type ErrorCode = Int 16 | type ErrorMessage = ByteString 17 | 18 | 19 | data RunHaskellError = 20 | RunHaskellNotFound | RunHaskellFailure ErrorCode ErrorMessage deriving (Show, Typeable) 21 | 22 | instance Exception RunHaskellError 23 | 24 | data HasParens = 25 | NoParens | OnlyOpenParen | BothParens 26 | deriving (Eq, Show) 27 | 28 | 29 | data SingleArgFunApp = SingleArgFunApp 30 | { singleArgFunAppFunName :: !TextS.Text 31 | , singleArgFunAppArg :: !(Maybe SingleArgFunApp) 32 | , singleArgFunAppHasParen :: !HasParens 33 | } deriving (Eq, Show) 34 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise 4 | ( Exercise(verify, name) 5 | , Name 6 | , Result(..) 7 | , Details 8 | , loadHeaders 9 | , loadDescriptionByName 10 | , loadExampleSolution 11 | , loadLastShown 12 | , saveLastShownName 13 | , unsafeGetByName 14 | ) where 15 | 16 | 17 | #include 18 | 19 | import Education.MakeMistakesToLearnHaskell.Exercise.Core 20 | import Education.MakeMistakesToLearnHaskell.Diagnosis 21 | import Education.MakeMistakesToLearnHaskell.Env 22 | import Education.MakeMistakesToLearnHaskell.Evaluator.Regex 23 | import qualified Education.MakeMistakesToLearnHaskell.Evaluator.RunHaskell as RunHaskell 24 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 25 | import Education.MakeMistakesToLearnHaskell.Exercise.FormatMessage 26 | import Education.MakeMistakesToLearnHaskell.Exercise.Record 27 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 28 | import Education.MakeMistakesToLearnHaskell.Error 29 | import Education.MakeMistakesToLearnHaskell.Text 30 | 31 | -- 課題 32 | import Education.MakeMistakesToLearnHaskell.Exercise.Ex01 33 | import Education.MakeMistakesToLearnHaskell.Exercise.Ex02 34 | import Education.MakeMistakesToLearnHaskell.Exercise.Ex02_5 35 | import Education.MakeMistakesToLearnHaskell.Exercise.Ex03 36 | import Education.MakeMistakesToLearnHaskell.Exercise.Ex04 37 | import Education.MakeMistakesToLearnHaskell.Exercise.Ex05 38 | 39 | exercises :: [(Name, Exercise)] 40 | exercises = map (\e -> (name e, e)) 41 | [ exercise1 42 | , exercise2 43 | , exercise2_5 44 | , exercise3 45 | , exercise4 46 | , exercise5 47 | ] 48 | 49 | loadHeaders :: IO [Text] 50 | loadHeaders = mapM (loadHeader . snd) exercises 51 | where 52 | loadHeader ex = extractHeader ex =<< loadDescription ex 53 | 54 | extractHeader ex desc = 55 | dieWhenNothing ("The description of exercise '" ++ name ex ++ "' is empty!") 56 | $ appendName ex . cutHash <$> headMay (Text.lines desc) 57 | 58 | cutHash h = 59 | Text.strip $ fromMaybe h $ Text.stripPrefix "# " h 60 | 61 | appendName ex h = 62 | Text.pack (name ex) <> ": " <> h 63 | 64 | 65 | loadDescription :: Exercise -> IO Text 66 | loadDescription = loadWithExtension ".md" 67 | 68 | 69 | loadExampleSolution :: Exercise -> IO Text 70 | loadExampleSolution = loadWithExtension ".hs" 71 | 72 | 73 | loadWithExtension :: String -> Exercise -> IO Text 74 | loadWithExtension ext ex = 75 | Paths.getDataFileName ("assets/" ++ name ex ++ ext) 76 | >>= readUtf8File 77 | 78 | 79 | loadDescriptionByName :: Name -> IO (Maybe Text) 80 | loadDescriptionByName n = MaybeT.runMaybeT $ do 81 | ex <- Error.hoistMaybe $ getByName n 82 | liftIO $ loadDescription ex 83 | 84 | 85 | -- Handle error internally. 86 | -- Because lastShownName is usually saved internally. 87 | loadLastShown :: Env -> IO Exercise 88 | loadLastShown e = 89 | loadLastShownName e >>= 90 | dieWhenNothing "Assertion failure: Invalid lastShownName saved! " . getByName 91 | 92 | 93 | getByName :: Name -> Maybe Exercise 94 | getByName n = lookup n exercises 95 | 96 | 97 | unsafeGetByName :: Name -> Exercise 98 | unsafeGetByName = fromMaybe (error "unsafeGetByName: No exercise found!") . getByName 99 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Core.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Core 4 | ( runHaskellExercise 5 | , runHaskellExerciseWithStdin 6 | , noVeirificationExercise 7 | , notYetImplementedVeirificationExercise 8 | , isInWords 9 | , detailsForgetToWriteDo 10 | , detailsDoConsistentWidth 11 | , isInconsistentlyIndentedAfter 12 | ) where 13 | 14 | #include 15 | 16 | import Education.MakeMistakesToLearnHaskell.Env 17 | import qualified Education.MakeMistakesToLearnHaskell.Evaluator.RunHaskell as RunHaskell 18 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 19 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 20 | import Education.MakeMistakesToLearnHaskell.Diagnosis 21 | import Education.MakeMistakesToLearnHaskell.Text 22 | 23 | runHaskellExercise 24 | :: Diagnosis 25 | -> Text 26 | -> Env 27 | -> FilePath 28 | -> IO Result 29 | runHaskellExercise = runHaskellExercise' Nothing 30 | 31 | -- TODO: refactor with resultForUser 32 | runHaskellExercise' 33 | :: Maybe RunHaskellParameters 34 | -> Diagnosis 35 | -> Text 36 | -> Env 37 | -> FilePath 38 | -> IO Result 39 | runHaskellExercise' mParam diag right e prgFile = do 40 | let rhp = fromMaybe defaultRunHaskellParameters mParam 41 | result <- runHaskell e $ rhp { runHaskellParametersArgs = [prgFile] } 42 | case result of 43 | Right (outB, _errB {- TODO: print stderr -}) -> do 44 | let out = canonicalizeNewlines outB 45 | msg = 46 | Text.unlines 47 | [ Text.replicate 80 "=" 48 | , "Your program's output: " <> Text.pack (show out) -- TODO: pretty print 49 | , " Expected output: " <> Text.pack (show right) 50 | ] 51 | return $ 52 | if out == right 53 | then Success $ "Nice output!\n\n" <> msg 54 | else Fail $ "Wrong output!\n\n" <> msg 55 | Left err -> 56 | case err of 57 | RunHaskell.RunHaskellNotFound -> 58 | return $ Error "runhaskell command is not available.\nInstall stack or Haskell Platform." 59 | RunHaskell.RunHaskellFailure _ msg -> do 60 | logDebug e $ "RunHaskellFailure: " <> msg 61 | code <- readUtf8File prgFile 62 | putStrLn "==================== GHC output ====================" 63 | return $ Fail $ appendDiagnosis diag code msg 64 | 65 | -- runHaskellExercise の入力有りバージョン 66 | runHaskellExerciseWithStdin 67 | :: Diagnosis 68 | -> Gen String 69 | -> (Text -> Text) 70 | -> Env 71 | -> FilePath 72 | -> IO Result 73 | runHaskellExerciseWithStdin diag gen calcRight env prgFile = do 74 | let qcArgs = QuickCheck.stdArgs { QuickCheck.chatty = True } 75 | maxSuccessSize = envQcMaxSuccessSize env 76 | 77 | resultRef <- newIORef $ error "Assertion failure: no result written after QuickCheck" 78 | qr <- quickCheckWithResult qcArgs $ 79 | QuickCheck.forAll gen $ \inputS -> 80 | QuickCheck.withMaxSuccess maxSuccessSize $ 81 | QuickCheck.ioProperty $ do 82 | let input = Text.pack inputS 83 | params = defaultRunHaskellParameters 84 | { runHaskellParametersArgs = [prgFile] 85 | , runHaskellParametersStdin = TextEncoding.encodeUtf8 input 86 | } 87 | code <- readUtf8File prgFile 88 | result <- resultForUser diag code [" For input: " <> Text.pack (show input)] calcRight input <$> runHaskell env params 89 | writeIORef resultRef result 90 | return $ 91 | case result of 92 | Success _ -> True 93 | _other -> False 94 | logDebug env $ ByteString.pack $ "QuickCheck result: " ++ show qr 95 | readIORef resultRef 96 | 97 | resultForUser 98 | :: Diagnosis 99 | -> Text 100 | -> [Text] 101 | -> (Text -> Text) 102 | -> Text 103 | -> Either RunHaskellError (ByteString, ByteString) 104 | -> Result 105 | resultForUser _diag _code messageFooter calcRight input (Right (outB, _errB {- TODO: print stderr -})) = 106 | let out = canonicalizeNewlines outB 107 | right = calcRight input 108 | msg = 109 | Text.unlines $ 110 | [ Text.replicate 80 "=" 111 | , "Your program's output: " <> Text.pack (show out) -- TODO: pretty print 112 | , " Expected output: " <> Text.pack (show right) 113 | ] ++ messageFooter 114 | in 115 | if right == out 116 | then Success $ "Nice output!\n\n" <> msg 117 | else Fail $ "Wrong output!\n\n" <> msg 118 | resultForUser _diag _code _messageFooter _calcRight _minput (Left RunHaskell.RunHaskellNotFound) = 119 | Error "runhaskell command is not available.\nInstall stack or Haskell Platform." 120 | resultForUser diag code _messageFooter _calcRight _minput (Left (RunHaskell.RunHaskellFailure _ msg)) = 121 | Fail $ appendDiagnosis diag code msg 122 | 123 | isInWords :: Text -> [Text] -> Bool 124 | isInWords wd = any (Text.isInfixOf wd) 125 | 126 | detailsForgetToWriteDo :: Text -> Details 127 | detailsForgetToWriteDo funcNames = 128 | "HINT: You seem to have forgotten to write `do`. `do` must be put before listing " <> funcNames <> "." 129 | 130 | detailsDoConsistentWidth :: Details 131 | detailsDoConsistentWidth = "HINT: instructions in a `do` must be in a consistent width." 132 | 133 | isInconsistentlyIndentedAfter :: SourceCode -> Text -> Bool 134 | isInconsistentlyIndentedAfter code wd = 135 | not 136 | $ allSame 137 | $ map (Text.length . Text.takeWhile Char.isSpace) 138 | $ cropAfterWord wd 139 | $ Text.lines code 140 | where 141 | cropAfterWord :: Text -> [SourceCode] -> [SourceCode] 142 | cropAfterWord w ls = 143 | -- Against my expectaion, 144 | -- 'dropWhile (isInWords w . Text.words) ls' returns ls as is. 145 | -- While this function should return an empty list 146 | -- if 'ls' doesn't contain 'w'. 147 | let (_nonContaining, containing) = List.break (isInWords w . Text.words) ls 148 | in 149 | if null containing 150 | then [] 151 | else drop 1 containing -- except the first line, which contains 'w' 152 | 153 | allSame :: Eq a => [a] -> Bool 154 | allSame [] = True 155 | allSame [_] = True 156 | allSame (x1 : x2 : xs) = x1 == x2 && allSame xs 157 | 158 | noVeirificationExercise :: Env -> String -> IO Result 159 | noVeirificationExercise _ _ = return NotVerified 160 | 161 | notYetImplementedVeirificationExercise :: Env -> String -> IO Result 162 | notYetImplementedVeirificationExercise _ _ = return NotYetImplemented 163 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Ex01.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Ex01 4 | ( exercise1 5 | ) where 6 | 7 | #include 8 | 9 | import Education.MakeMistakesToLearnHaskell.Exercise.Core 10 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 11 | 12 | exercise1 :: Exercise 13 | exercise1 = 14 | Exercise "1" $ runHaskellExercise diag1 "Hello, world!\n" 15 | 16 | diag1 :: Diagnosis 17 | diag1 code msg 18 | | "parse error on input" `Text.isInfixOf` msg 19 | && "'" `Text.isInfixOf` code = 20 | "HINT: In Haskell, you must surround string literals with double-quotes '\"', like \"Hello, world\"." 21 | | ("parse error" `Text.isInfixOf` msg || "Parse error" `Text.isInfixOf` msg) 22 | && "top-level declaration expected." `Text.isInfixOf` msg = 23 | "HINT: This error indicates that you haven't defined the main function." 24 | | "Variable not in scope: main :: IO" `Text.isInfixOf` msg = 25 | "HINT: This error indicates that you haven't defined the main function." 26 | | "Variable not in scope:" `Text.isInfixOf` msg = 27 | "HINT: you might have misspelled 'putStrLn'." 28 | | otherwise = "" 29 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Ex02.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Ex02 4 | ( exercise2 5 | ) where 6 | 7 | #include 8 | 9 | import Education.MakeMistakesToLearnHaskell.Exercise.Core 10 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 11 | 12 | exercise2 :: Exercise 13 | exercise2 = Exercise "2" $ runHaskellExercise diag2 "20.761245674740486\n" 14 | 15 | diag2 :: Diagnosis 16 | diag2 code msg 17 | | "parse error" `Text.isInfixOf` msg || "Parse error" `Text.isInfixOf` msg = 18 | if "top-level declaration expected." `Text.isInfixOf` msg 19 | then 20 | "HINT: This error indicates that you haven't defined the main function." 21 | else 22 | -- TODO: Use regex or ghc tokenizer 23 | case compare (Text.count "(" code) (Text.count ")" code) of 24 | GT -> "HINT: you might have forgotten to write a close parenthesis" 25 | LT -> "HINT: you might have forgotten to write an open parenthesis" 26 | EQ -> "" 27 | | "No instance for (Fractional (IO ()))" `Text.isInfixOf` msg || "No instance for (Num (IO ()))" `Text.isInfixOf` msg = 28 | "HINT: you might have forgotten to write parentheses" 29 | | "No instance for (Show (a0 -> a0))" `Text.isInfixOf` msg = 30 | "HINT: you might have forgotten to write some numbers between operators ('*', '/' etc.)." 31 | | "No instance for (Num (t0 -> a0))" `Text.isInfixOf` msg = 32 | "HINT: you might have forgotten to write the multiplication operator '*'" 33 | | "No instance for (Fractional (t0 -> a0))" `Text.isInfixOf` msg = 34 | "HINT: you might have forgotten to write the division operator '/'" 35 | | "Variable not in scope: main :: IO" `Text.isInfixOf` msg = 36 | "HINT: This error indicates that you haven't defined the main function." 37 | | otherwise = "" 38 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Ex02_5.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Ex02_5 4 | ( exercise2_5 5 | ) where 6 | 7 | #include 8 | 9 | import Education.MakeMistakesToLearnHaskell.Exercise.Core 10 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 11 | 12 | exercise2_5 :: Exercise 13 | exercise2_5 = Exercise "2.5" noVeirificationExercise 14 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Ex03.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Ex03 4 | ( exercise3 5 | ) where 6 | 7 | #include 8 | 9 | import Education.MakeMistakesToLearnHaskell.Exercise.Core 10 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 11 | 12 | exercise3 :: Exercise 13 | exercise3 = Exercise "3" $ runHaskellExercise diag3 $ Text.unlines 14 | [ "# # ####### # # #####" 15 | , "# # # # # # #" 16 | , "# # # # # # #" 17 | , "####### ##### # # # #" 18 | , "# # # # # # #" 19 | , "# # # # # # #" 20 | , "# # ####### ####### ####### #####" 21 | ] 22 | 23 | diag3 :: Diagnosis 24 | diag3 code msg 25 | | code `isInconsistentlyIndentedAfter` "do" = detailsDoConsistentWidth 26 | | "parse error on input" `Text.isInfixOf` msg 27 | && "'" `Text.isInfixOf` code = 28 | "HINT: In Haskell, you must surround string literals with double-quotes '\"', like \"Hello, world\"." 29 | | ("parse error" `Text.isInfixOf` msg || "Parse error" `Text.isInfixOf` msg) 30 | && "top-level declaration expected." `Text.isInfixOf` msg = 31 | "HINT: This error indicates that you haven't defined the main function." 32 | | "Variable not in scope: main :: IO" `Text.isInfixOf` msg = 33 | "HINT: This error indicates that you haven't defined the main function." 34 | | "Variable not in scope:" `Text.isInfixOf` msg = 35 | "HINT: you might have misspelled 'putStrLn'." 36 | | "Couldn't match expected type ‘(String -> IO ())" `Text.isInfixOf` msg = 37 | detailsForgetToWriteDo "`putStrLn`s" 38 | | otherwise = "" 39 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Ex04.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Ex04 4 | ( exercise4 5 | ) where 6 | 7 | #include 8 | 9 | import Education.MakeMistakesToLearnHaskell.Evaluator.Regex 10 | import Education.MakeMistakesToLearnHaskell.Exercise.Core 11 | import Education.MakeMistakesToLearnHaskell.Exercise.FormatMessage 12 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 13 | 14 | exercise4 :: Exercise 15 | exercise4 = Exercise "4" 16 | $ runHaskellExerciseWithStdin diag4 gen4 17 | $ (Text.pack . unlines . reverse . lines . Text.unpack) 18 | 19 | gen4 :: Gen String 20 | gen4 = unlines <$> QuickCheck.listOf (QuickCheck.listOf $ QuickCheck.choose ('\33', '\126')) 21 | 22 | diag4 :: Diagnosis 23 | diag4 code msg 24 | | code `isInconsistentlyIndentedAfter` "do" = 25 | detailsDoConsistentWidth 26 | | "Perhaps this statement should be within a 'do' block?" `Text.isInfixOf` msg = 27 | if hasNoMainFirst code then 28 | "HINT: Your source code dosn't have `main` function!" -- TODO: Rewrite other no-main cases with this. 29 | else if code `containsSequence` ["main", "<-"] then 30 | "HINT: Don't use `<-` to define the `main` function. Use `=` instead." 31 | else 32 | detailsForgetToWriteDo "`putStr`s and `getContents`" 33 | | "Perhaps you need a 'let' in a 'do' block?" `Text.isInfixOf` msg 34 | && code `containsSequence` ["=", "getContents"] = 35 | "HINT: Don't assign the result of `getContents` with `=`. Use `<-` instead." 36 | | "Couldn't match type ‘IO String’ with ‘[Char]’" `Text.isInfixOf` msg 37 | && "In the first argument of ‘lines’" `Text.isInfixOf` msg = 38 | "HINT: You have to assign the result of `getContents` with `<-` operator." 39 | | otherwise = 40 | let mtoks = GHC.tokenizeHaskell (Text.toStrict code) 41 | tokPutStr = (GHC.VariableTok, "putStr") 42 | putStrThenSpace = 43 | Regex.sym tokPutStr <* optional (Regex.psym ((== GHC.SpaceTok) . fst)) 44 | msafa = 45 | matchSub (singleArgFunApp 5) . (tokPutStr :) . dropUntilFirst putStrThenSpace =<< mtoks 46 | in 47 | case msafa of 48 | Just safa -> Text.unlines $ formatSingleArgFunApp safa 49 | _ -> "" 50 | 51 | -- TODO: Incomplete implementaion! Use regex or ghc tokenizer! 52 | containsSequence :: SourceCode -> [Text] -> Bool 53 | containsSequence code wds = Text.concat wds `isInWords` ws || (wds `List.isInfixOf` ws) 54 | where 55 | ws = Text.words code 56 | 57 | hasNoMainFirst :: SourceCode -> Bool 58 | hasNoMainFirst src = 59 | case Text.words src of 60 | [] -> True 61 | (h : _) -> not $ "main" `Text.isPrefixOf` h 62 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Ex05.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Ex05 4 | ( exercise5 5 | ) where 6 | 7 | #include 8 | 9 | import Education.MakeMistakesToLearnHaskell.Exercise.Core 10 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 11 | 12 | 13 | exercise5 :: Exercise 14 | exercise5 = Exercise "5" 15 | $ runHaskellExerciseWithStdin diag generator answer 16 | 17 | 18 | diag :: Diagnosis 19 | diag _code _msg = "" -- TODO: Not implemented 20 | 21 | 22 | generator :: Gen String 23 | generator = 24 | unlines <$> sequence 25 | [ show <$> (arbitrary :: Gen Double) 26 | , show <$> (arbitrary :: Gen Double) 27 | , show . QuickCheck.getPositive <$> (arbitrary :: Gen (QuickCheck.Positive Int)) 28 | ] 29 | 30 | 31 | answer :: Text -> Text 32 | answer input = Text.pack $ show (body :: Double) <> "\n" 33 | where 34 | [principal, interestRate, years] = lines $ Text.unpack input 35 | body = read principal * (1 + read interestRate / 100) ^ (read years :: Integer) 36 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/FormatMessage.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.FormatMessage 4 | ( formatSingleArgFunApp 5 | ) where 6 | 7 | 8 | #include 9 | 10 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 11 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 12 | 13 | 14 | formatSingleArgFunApp :: SingleArgFunApp -> [Details] 15 | formatSingleArgFunApp = List.unfoldr uf 16 | where 17 | uf safa1 = 18 | let f safa2 = 19 | let hint = 20 | case singleArgFunAppHasParen safa2 of 21 | BothParens -> "" 22 | NoParens -> 23 | "HINT: No parentheses between " 24 | <> singleArgFunAppFunName safa1 25 | <> " and " 26 | <> singleArgFunAppFunName safa2 27 | <> "." 28 | OnlyOpenParen -> 29 | "HINT: The open parenthesis between " 30 | <> singleArgFunAppFunName safa1 31 | <> " and " 32 | <> singleArgFunAppFunName safa2 33 | <> " is not closed." 34 | in 35 | -- NOTE: The final argument should not be tested because it should be a raw expression. 36 | -- E.g. The `input` argument of `reverse (lines input)`. 37 | if isJust $ singleArgFunAppArg safa2 38 | then Just (Text.fromStrict hint, safa2) 39 | else Nothing 40 | in f =<< singleArgFunAppArg safa1 41 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Record.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Record 4 | ( loadLastShownName 5 | , saveLastShownName 6 | ) where 7 | 8 | #include 9 | 10 | import Education.MakeMistakesToLearnHaskell.Env 11 | import Education.MakeMistakesToLearnHaskell.Exercise.Types 12 | import Education.MakeMistakesToLearnHaskell.Error 13 | 14 | 15 | loadLastShownName :: Env -> IO Name 16 | loadLastShownName e = do 17 | path <- prepareRecordFilePath e 18 | exists <- Dir.doesFileExist path 19 | if exists then 20 | fmap (lastShownName . read) $ readFile path 21 | else 22 | return "1" 23 | 24 | 25 | saveLastShownName :: Env -> Name -> IO () 26 | saveLastShownName e n = do 27 | path <- prepareRecordFilePath e 28 | writeFile path $ show $ Record n 29 | 30 | 31 | prepareRecordFilePath :: Env -> IO FilePath 32 | prepareRecordFilePath e = do 33 | let d = appHomePath e dirName 34 | Dir.createDirectoryIfMissing True d 35 | return $ d "record.yaml" 36 | 37 | 38 | dirName :: FilePath 39 | dirName = "Exercise" 40 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Exercise/Types.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise.Types where 4 | 5 | #include 6 | 7 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 8 | import Education.MakeMistakesToLearnHaskell.Env 9 | 10 | data Exercise = 11 | Exercise 12 | { name :: !Name 13 | -- ^ The name of the exercise. 14 | , verify :: Env -> String -> IO Result 15 | -- ^ The function to verify the source file, project directory, 16 | -- or any string pointing to the user's answer. 17 | -- So, the second argument's @String@ is something 18 | -- pointing to the user's answer. 19 | } 20 | 21 | 22 | data Result = 23 | Error !Details 24 | | Fail !Details 25 | | Success !Details 26 | | NotVerified 27 | | NotYetImplemented 28 | deriving (Eq, Show) 29 | 30 | newtype Record = Record 31 | { lastShownName :: Name 32 | } deriving (Show, Read) 33 | 34 | type Details = Text 35 | 36 | type SourceCode = Text 37 | 38 | type Name = String 39 | 40 | type Diagnosis = SourceCode -> Details -> Details 41 | -------------------------------------------------------------------------------- /src/Education/MakeMistakesToLearnHaskell/Text.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Text 4 | ( canonicalizeNewlines 5 | , decodeUtf8 6 | , readUtf8File 7 | , writeUtf8FileS 8 | , removeAllTrailingSpace 9 | ) where 10 | 11 | 12 | #include 13 | 14 | 15 | canonicalizeNewlines :: ByteString -> Text 16 | canonicalizeNewlines = Text.replace "\r\n" "\n" . decodeUtf8 17 | 18 | 19 | decodeUtf8 :: ByteString -> Text 20 | decodeUtf8 = TextEncoding.decodeUtf8With handler 21 | where 22 | handler _ (Just _) = Just ' ' 23 | handler cause nothing = 24 | throw $ TextEncoding.DecodeError cause nothing 25 | 26 | 27 | readUtf8File :: FilePath -> IO Text 28 | readUtf8File path = do 29 | hd <- IO.openFile path IO.ReadMode 30 | IO.hSetEncoding hd IO.utf8 31 | Text.hGetContents hd 32 | 33 | 34 | writeUtf8FileS :: FilePath -> TextS.Text -> IO () 35 | writeUtf8FileS path dat = 36 | IO.withFile path IO.WriteMode $ \hd -> do 37 | IO.hSetEncoding hd IO.utf8 38 | TextS.hPutStr hd dat 39 | 40 | 41 | removeAllTrailingSpace :: Text -> Text 42 | removeAllTrailingSpace = Text.unlines . map Text.stripEnd . Text.lines 43 | -------------------------------------------------------------------------------- /src/imports/external.hs: -------------------------------------------------------------------------------- 1 | -- Common import statements for external libraries. 2 | -- Intended to include by CPP. 3 | 4 | import qualified CMark 5 | import Control.Applicative ((<|>), optional, (<**>)) 6 | import qualified Control.Error as Error 7 | import Control.Exception 8 | ( Exception 9 | , SomeException 10 | , bracket_ 11 | , catch 12 | , throwIO 13 | , throw 14 | ) 15 | import Control.Monad (void, unless) 16 | import Control.Monad.IO.Class (liftIO) 17 | import qualified Control.Monad.Trans.Maybe as MaybeT 18 | import Data.Bool (bool) 19 | import Data.ByteString.Lazy.Char8 (ByteString) 20 | import qualified Data.ByteString.Lazy.Char8 as ByteString 21 | import qualified Data.Char as Char 22 | import Data.Functor (($>)) 23 | import qualified Data.List as List 24 | import Data.Maybe (fromMaybe, maybeToList, isJust) 25 | import Data.IORef 26 | ( newIORef 27 | , readIORef 28 | , writeIORef 29 | ) 30 | import Data.Monoid ((<>)) 31 | import qualified Data.Text.Encoding.Error as TextEncoding 32 | import Data.Text.Lazy (Text) 33 | import qualified Data.Text.Lazy as Text 34 | import qualified Data.Text.Lazy.IO as Text 35 | import qualified Data.Text.Lazy.Encoding as TextEncoding 36 | import qualified Data.Text as TextS 37 | import qualified Data.Text.IO as TextS 38 | import Data.Typeable (Typeable) 39 | import qualified Debug.Trace as Debug 40 | import GHC.Generics (Generic) 41 | import qualified GHC.SyntaxHighlighter as GHC 42 | import Numeric.Natural (Natural) 43 | import qualified Paths_makeMistakesToLearnHaskell as Paths 44 | import Safe (headMay) 45 | import qualified System.Directory as Dir 46 | import qualified System.Environment as Env 47 | import qualified System.Exit as Exit 48 | import System.Exit (ExitCode(ExitSuccess, ExitFailure)) 49 | import System.FilePath (()) 50 | import qualified System.IO as IO 51 | import System.Process.Typed (readProcess) 52 | import qualified System.Process.Typed as Process 53 | import qualified Test.QuickCheck as QuickCheck 54 | import Test.QuickCheck (Arbitrary, Gen, quickCheckWithResult, arbitrary) 55 | import qualified Text.Regex.Applicative as Regex 56 | import qualified Web.Browser as Browser 57 | #ifdef mingw32_HOST_OS 58 | import qualified System.Win32.Console as Win32 59 | import GHC.IO.Encoding.CodePage (mkLocaleEncoding) 60 | import GHC.IO.Encoding.Failure (CodingFailureMode(TransliterateCodingFailure)) 61 | #endif 62 | -------------------------------------------------------------------------------- /src/test/imports/external.hs: -------------------------------------------------------------------------------- 1 | import Test.Hspec 2 | import Test.Hspec.Core.Spec (SpecM) 3 | import Test.Main 4 | ( captureProcessResult 5 | , withArgs 6 | , withEnv 7 | , ProcessResult(ProcessResult, prStdout, prStderr, prExitCode, prException) 8 | ) 9 | 10 | import qualified Paths_makeMistakesToLearnHaskell as Paths 11 | 12 | import Control.Monad (void) 13 | import Control.Exception (try) 14 | import Data.ByteString.Lazy.Char8 (ByteString) 15 | import qualified Data.ByteString.Lazy.Char8 as ByteString 16 | import qualified Data.ByteString.Char8 as ByteString' 17 | import Data.Foldable (for_) 18 | import Data.Function ((&)) 19 | import qualified Data.Text.Lazy as Text 20 | import qualified System.Directory as Dir 21 | import qualified System.Environment as Env 22 | import System.Exit (ExitCode(ExitSuccess, ExitFailure)) 23 | import System.FilePath (()) 24 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # http://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 15 | # resolver: 16 | # name: custom-snapshot 17 | # location: "./custom-snapshot.yaml" 18 | resolver: lts-12.10 19 | 20 | # User packages to be built. 21 | # Various formats can be used as shown in the example below. 22 | # 23 | # packages: 24 | # - some-directory 25 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 26 | # - location: 27 | # git: https://github.com/commercialhaskell/stack.git 28 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 29 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 30 | # extra-dep: true 31 | # subdirs: 32 | # - auto-update 33 | # - wai 34 | # 35 | # A package marked 'extra-dep: true' will only be built if demanded by a 36 | # non-dependency (i.e. a user package), and its test suites and benchmarks 37 | # will not be run. This is useful for tweaking upstream packages. 38 | packages: 39 | - '.' 40 | # Dependency packages to be pulled from upstream that are not in the resolver 41 | # (e.g., acme-missiles-0.3) 42 | extra-deps: 43 | - main-tester-0.2.0.0 44 | - NoTrace-0.3.0.3 45 | - cmark-0.5.6 46 | 47 | # Override default flag values for local packages and extra-deps 48 | flags: {} 49 | 50 | # Extra package databases containing global packages 51 | extra-package-dbs: [] 52 | 53 | # Control whether we use the GHC we find on the path 54 | # system-ghc: true 55 | # 56 | # Require a specific version of stack, using version ranges 57 | # require-stack-version: -any # Default 58 | # require-stack-version: ">=1.3" 59 | # 60 | # Override the architecture used by stack, especially useful on Windows 61 | # arch: i386 62 | # arch: x86_64 63 | # 64 | # Extra directories used by stack for building 65 | # extra-include-dirs: [/path/to/dir] 66 | # extra-lib-dirs: [/path/to/dir] 67 | # 68 | # Allow a newer minor version of GHC than the snapshot specifies 69 | # compiler-check: newer-minor 70 | -------------------------------------------------------------------------------- /test/Education/MakeMistakesToLearnHaskell/Exercise1Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise1Spec 4 | ( spec 5 | , main 6 | ) where 7 | 8 | #include 9 | 10 | import Education.MakeMistakesToLearnHaskell.Env 11 | import Education.MakeMistakesToLearnHaskell.SpecEnv 12 | import Education.MakeMistakesToLearnHaskell.SpecHelper 13 | 14 | import qualified Education.MakeMistakesToLearnHaskell.Exercise as Exercise 15 | 16 | 17 | -- `main` is here so that this module can be run from GHCi on its own. It is 18 | -- not needed for automatic spec discovery. 19 | main :: IO () 20 | main = hspec spec 21 | 22 | spec :: Spec 23 | spec = do 24 | itShouldFailForCaseWithMessage 25 | "1" 26 | "single-quote" 27 | ["HINT: In Haskell, you must surround string literals with double-quotes '\"', like \"Hello, world\"."] 28 | 29 | itShouldFailForCaseWithMessage 30 | "1" 31 | "no-main" 32 | ["HINT: This error indicates that you haven't defined the main function."] 33 | 34 | itShouldFailForCaseWithMessage "1" "typo" [] 35 | -------------------------------------------------------------------------------- /test/Education/MakeMistakesToLearnHaskell/Exercise2Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise2Spec 4 | ( spec 5 | , main 6 | ) where 7 | 8 | #include 9 | 10 | import Education.MakeMistakesToLearnHaskell.Env 11 | import Education.MakeMistakesToLearnHaskell.SpecEnv 12 | import Education.MakeMistakesToLearnHaskell.SpecHelper 13 | 14 | import qualified Education.MakeMistakesToLearnHaskell.Exercise as Exercise 15 | 16 | 17 | -- `main` is here so that this module can be run from GHCi on its own. It is 18 | -- not needed for automatic spec discovery. 19 | main :: IO () 20 | main = hspec spec 21 | 22 | spec :: Spec 23 | spec = do 24 | baseEnv <- mkDefaultSpecEnv 25 | let subject = Exercise.unsafeGetByName "2" 26 | 27 | it "given the correct answer, show SUCCESS" $ do 28 | out <- ByteString.readFile "test/assets/2/error-messages/correct.txt" 29 | let e = setRunHaskellSuccessWithStdout baseEnv out 30 | void $ shouldSuccess =<< Exercise.verify subject e "assets/2.hs" 31 | 32 | itShouldFailForCaseWithMessage 33 | "2" 34 | "no-number-1" 35 | ["HINT: you might have forgotten to write some numbers between operators ('*', '/' etc.)."] 36 | 37 | itShouldFailForCaseWithMessage 38 | "2" 39 | "no-number-2" 40 | ["HINT: you might have forgotten to write some numbers between operators ('*', '/' etc.)."] 41 | 42 | itShouldFailForCaseWithMessage 43 | "2" 44 | "no-number-3" 45 | ["HINT: you might have forgotten to write some numbers between operators ('*', '/' etc.)."] 46 | 47 | itShouldFailForCaseWithMessage 48 | "2" 49 | "no-paren" 50 | ["HINT: you might have forgotten to write parentheses"] 51 | 52 | itShouldFailForCaseWithMessage 53 | "2" 54 | "no-close-paren" 55 | ["HINT: you might have forgotten to write a close parenthesis"] 56 | 57 | itShouldFailForCaseWithMessage 58 | "2" 59 | "no-main" 60 | ["HINT: This error indicates that you haven't defined the main function."] 61 | 62 | itShouldFailForCaseWithMessage 63 | "2" 64 | "no-open-paren" 65 | ["HINT: you might have forgotten to write an open parenthesis"] 66 | 67 | itShouldFailForCaseWithMessage 68 | "2" 69 | "no-slash" 70 | ["HINT: you might have forgotten to write the division operator '/'"] 71 | 72 | itShouldFailForCaseWithMessage 73 | "2" 74 | "no-star" 75 | ["HINT: you might have forgotten to write the multiplication operator '*'"] 76 | 77 | itShouldFailForCaseWithMessage "2" "typo" [] 78 | 79 | it "given an answer printing wrong result, show FAIL" $ do 80 | err <- ByteString.readFile "test/assets/2/error-messages/wrong-number.txt" 81 | let e = setRunHaskellSuccessWithStdout baseEnv err 82 | d <- shouldFail =<< Exercise.verify subject e "test/assets/2/wrong-number.hs" 83 | d `shouldSatisfy` Text.isInfixOf "Your program's output: \"6.0\\n\"" 84 | d `shouldSatisfy` Text.isInfixOf "Expected output: \"20.761245674740486\\n\"" 85 | -------------------------------------------------------------------------------- /test/Education/MakeMistakesToLearnHaskell/Exercise3Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise3Spec 4 | ( spec 5 | , main 6 | ) where 7 | 8 | #include 9 | 10 | import Education.MakeMistakesToLearnHaskell.Env 11 | import Education.MakeMistakesToLearnHaskell.SpecEnv 12 | import Education.MakeMistakesToLearnHaskell.SpecHelper 13 | 14 | import qualified Education.MakeMistakesToLearnHaskell.Exercise as Exercise 15 | 16 | 17 | -- `main` is here so that this module can be run from GHCi on its own. It is 18 | -- not needed for automatic spec discovery. 19 | main :: IO () 20 | main = hspec spec 21 | 22 | spec :: Spec 23 | spec = do 24 | baseEnv <- mkDefaultSpecEnv 25 | let subject = Exercise.unsafeGetByName "3" 26 | 27 | it "given the correct answer, show SUCCESS" $ do 28 | out <- ByteString.readFile "test/assets/3/error-messages/correct.txt" 29 | let e = setRunHaskellSuccessWithStdout baseEnv out 30 | void $ shouldSuccess =<< Exercise.verify subject e "assets/3.hs" 31 | 32 | itShouldFailForCaseWithMessage 33 | "3" 34 | "no-main" 35 | ["HINT: This error indicates that you haven't defined the main function."] 36 | 37 | itShouldFailForCaseWithMessage 38 | "3" 39 | "incornsistent-indent1" 40 | ["HINT: instructions in a `do` must be in a consistent width."] 41 | 42 | itShouldFailForCaseWithMessage 43 | "3" 44 | "incornsistent-indent2" 45 | ["HINT: instructions in a `do` must be in a consistent width."] 46 | 47 | itShouldFailForCaseWithMessage 48 | "3" 49 | "no-do" 50 | ["HINT: You seem to have forgotten to write `do`. `do` must be put before listing `putStrLn`s."] 51 | 52 | itShouldFailForCaseWithMessage 53 | "3" 54 | "single-quote" 55 | ["HINT: In Haskell, you must surround string literals with double-quotes '\"', like \"Hello, world\"."] 56 | 57 | itShouldFailForCaseWithMessage "3" "typo" [] 58 | 59 | it "given an answer printing wrong result, show FAIL" $ do 60 | err <- ByteString.readFile "test/assets/3/error-messages/wrong-output.txt" 61 | let e = setRunHaskellSuccessWithStdout baseEnv err 62 | d <- shouldFail =<< Exercise.verify subject e "test/assets/3/wrong-output.hs" 63 | d `shouldSatisfy` Text.isInfixOf "Your program's output:" -- TODO: better output 64 | d `shouldSatisfy` Text.isInfixOf "Expected output:" 65 | -------------------------------------------------------------------------------- /test/Education/MakeMistakesToLearnHaskell/Exercise4Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.Exercise4Spec 4 | ( spec 5 | , main 6 | ) where 7 | 8 | #include 9 | 10 | import Education.MakeMistakesToLearnHaskell.Env 11 | import Education.MakeMistakesToLearnHaskell.SpecEnv 12 | import Education.MakeMistakesToLearnHaskell.SpecHelper 13 | 14 | import qualified Education.MakeMistakesToLearnHaskell.Exercise as Exercise 15 | 16 | 17 | -- `main` is here so that this module can be run from GHCi on its own. It is 18 | -- not needed for automatic spec discovery. 19 | main :: IO () 20 | main = hspec spec 21 | 22 | spec :: Spec 23 | spec = do 24 | -- NOTE: SUCCESS and wrong-output case for exercise 4 is in the integration test 25 | -- to cover Education.MakeMistakesToLearnHaskell.Evaluator.RunHaskell.runFile with stdin. 26 | -- See Education.MakeMistakesToLearnHaskellSpec. 27 | 28 | itShouldFailForCaseWithMessage 29 | "4" 30 | "no-do" 31 | ["HINT: You seem to have forgotten to write `do`. `do` must be put before listing `putStr`s and `getContents`."] 32 | 33 | let useLeftThinArrow = "HINT: Don't assign the result of `getContents` with `=`. Use `<-` instead." 34 | -- ^ TODO: Collect common error messages 35 | 36 | itShouldFailForCaseWithMessage 37 | "4" 38 | "equal" 39 | [useLeftThinArrow] 40 | 41 | itShouldFailForCaseWithMessage 42 | "4" 43 | "equal-no-space" 44 | [useLeftThinArrow] 45 | 46 | itShouldFailForCaseWithMessage 47 | "4" 48 | "incornsistent-indent1" 49 | ["HINT: instructions in a `do` must be in a consistent width."] 50 | 51 | itShouldFailForCaseWithMessage 52 | "4" 53 | "incornsistent-indent2" 54 | ["HINT: instructions in a `do` must be in a consistent width."] 55 | 56 | itShouldFailForCaseWithMessage 57 | "4" 58 | "main-arrow" 59 | ["HINT: Don't use `<-` to define the `main` function. Use `=` instead."] 60 | 61 | itShouldFailForCaseWithMessage 62 | "4" 63 | "no-arrow" 64 | ["HINT: You have to assign the result of `getContents` with `<-` operator."] 65 | 66 | itShouldFailForCaseWithMessage 67 | "4" 68 | "no-close-paren" 69 | ["HINT: The open parenthesis between putStr and unlines is not closed."] 70 | 71 | itShouldFailForCaseWithMessage 72 | "4" 73 | "no-main" 74 | ["HINT: Your source code dosn't have `main` function!"] 75 | 76 | itShouldFailForCaseWithMessage 77 | "4" 78 | "no-open-paren1" 79 | ["HINT: No parentheses between putStr and unlines."] 80 | 81 | itShouldFailForCaseWithMessage 82 | "4" 83 | "no-open-paren2" 84 | ["HINT: No parentheses between unlines and reverse."] 85 | 86 | itShouldFailForCaseWithMessage 87 | "4" 88 | "no-open-paren3" 89 | ["HINT: No parentheses between reverse and lines."] 90 | 91 | itShouldFailForCaseWithMessage 92 | "4" 93 | "no-paren" 94 | [ "HINT: No parentheses between putStr and unlines" 95 | , "HINT: No parentheses between unlines and reverse." 96 | , "HINT: No parentheses between reverse and lines." 97 | ] 98 | 99 | itShouldFailForCaseWithMessage 100 | "4" 101 | "no-paren1" 102 | ["HINT: No parentheses between putStr and unlines."] 103 | 104 | itShouldFailForCaseWithMessage 105 | "4" 106 | "no-paren2" 107 | ["HINT: No parentheses between unlines and reverse."] 108 | 109 | itShouldFailForCaseWithMessage 110 | "4" 111 | "no-paren3" 112 | ["HINT: No parentheses between reverse and lines."] 113 | -------------------------------------------------------------------------------- /test/Education/MakeMistakesToLearnHaskell/SpecEnv.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.SpecEnv where 4 | 5 | 6 | #include 7 | 8 | import Education.MakeMistakesToLearnHaskell.Env 9 | import Education.MakeMistakesToLearnHaskell.Evaluator.Types 10 | 11 | 12 | mkDefaultSpecEnv :: SpecM a Env 13 | mkDefaultSpecEnv = runIO $ do 14 | tmpDir <- ( "tmp/mmlh") <$> Dir.getCurrentDirectory 15 | return $ defaultEnv 16 | { logDebug = const $ return () 17 | -- { logDebug = ByteString.putStrLn 18 | , appHomePath = tmpDir 19 | } 20 | 21 | 22 | setRunHaskellFailureWithOutput :: Env -> ByteString -> Env 23 | setRunHaskellFailureWithOutput e err = 24 | e { runHaskell = \_path -> return $ Left $ RunHaskellFailure 1 err } 25 | 26 | 27 | setRunHaskellSuccessWithStdout :: Env -> ByteString -> Env 28 | setRunHaskellSuccessWithStdout e out = 29 | e { runHaskell = \_path -> return $ Right (out, "") } 30 | 31 | 32 | setRunHaskellSuccessWithStdinFunction :: Env -> (ByteString -> ByteString) -> Env 33 | setRunHaskellSuccessWithStdinFunction e func = 34 | e { runHaskell = \rhp -> return $ Right (func $ runHaskellParametersStdin rhp, "") } 35 | -------------------------------------------------------------------------------- /test/Education/MakeMistakesToLearnHaskell/SpecHelper.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskell.SpecHelper where 4 | 5 | #include 6 | 7 | import qualified Education.MakeMistakesToLearnHaskell.Exercise as Exercise 8 | import Education.MakeMistakesToLearnHaskell.SpecEnv (setRunHaskellFailureWithOutput, mkDefaultSpecEnv) 9 | 10 | shouldFail :: Exercise.Result -> IO Exercise.Details 11 | shouldFail (Exercise.Fail d) = return d 12 | shouldFail other = fail $ "Unexpected exercise result: " ++ show other 13 | 14 | 15 | shouldSuccess :: Exercise.Result -> IO Exercise.Details 16 | shouldSuccess (Exercise.Success d) = return d 17 | shouldSuccess other = fail $ "Unexpected exercise result: " ++ show other 18 | 19 | 20 | type TestCaseId = String 21 | 22 | itShouldFailForCaseWithMessage :: Exercise.Name -> TestCaseId -> [Exercise.Details] -> SpecM () () 23 | itShouldFailForCaseWithMessage ename tcid messages = do 24 | baseEnv <- mkDefaultSpecEnv 25 | it (ename ++ "::" ++ tcid) $ do 26 | let subject = Exercise.unsafeGetByName ename 27 | err <- ByteString.readFile $ "test/assets/" ++ ename ++ "/error-messages/" ++ tcid ++ ".txt" 28 | let e = setRunHaskellFailureWithOutput baseEnv err 29 | d <- shouldFail =<< Exercise.verify subject e ("test/assets/" ++ ename ++ "/" ++ tcid ++ ".hs") 30 | 31 | for_ messages $ \message -> (Text.unpack d `shouldContain` Text.unpack message) -------------------------------------------------------------------------------- /test/Education/MakeMistakesToLearnHaskellSpec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-unused-imports #-} 2 | 3 | module Education.MakeMistakesToLearnHaskellSpec (main, spec) where 4 | 5 | #include 6 | 7 | import qualified Education.MakeMistakesToLearnHaskell 8 | import Education.MakeMistakesToLearnHaskell.Env 9 | 10 | -- `main` is here so that this module can be run from GHCi on its own. It is 11 | -- not needed for automatic spec discovery. 12 | main :: IO () 13 | main = hspec spec 14 | 15 | 16 | spec :: Spec 17 | spec = 18 | describe "mmlh verify" $ do 19 | it "given the correct answer of exercise 1, show SUCCESS" $ do 20 | void $ runMmlh ["show", "--terminal", "1"] 21 | answerFile <- Paths.getDataFileName ("assets" "1.hs") 22 | runMmlh ["verify", answerFile] >>= shouldVerifySuccess 23 | 24 | it "given an empty answer, show FAIL" $ do 25 | void $ runMmlh ["show", "--terminal", "1"] 26 | runMmlh ["verify", "test/assets/common/empty.hs"] 27 | >>= shouldExitWithHints ["HINT: This error indicates that you haven't defined the main function."] 28 | 29 | it "given non-existing answer of exercise 2.5, show NOT VERIFIED" $ do 30 | void $ runMmlh ["show", "--terminal", "2.5"] 31 | runMmlh ["verify", "non-existing"] >>= shouldPrintNotVerified 32 | 33 | it "given the correct answer of exercise 4, show SUCCESS" $ do 34 | answerFile <- Paths.getDataFileName ("assets" "4.hs") 35 | void $ runMmlh ["show", "--terminal", "4"] 36 | runMmlh ["verify", answerFile] >>= shouldVerifySuccess 37 | 38 | it "given a wrong answer of exercise 4, show FAIL" $ do 39 | let msgs = ["Your program's output:", "Expected output:"] 40 | void $ runMmlh ["show", "--terminal", "4"] 41 | runMmlh ["verify", "test/assets/4/wrong-output.hs"] 42 | >>= shouldExitWithHints msgs 43 | 44 | it "given a not-compilable answer of exercise 4, show FAIL" $ do 45 | let msgs = 46 | ["HINT: You seem to have forgotten to write `do`. `do` must be put before listing `putStr`s and `getContents`."] 47 | void $ runMmlh ["show", "--terminal", "4"] 48 | runMmlh ["verify", "test/assets/4/no-do.hs"] 49 | >>= shouldExitWithHints msgs 50 | 51 | 52 | runMmlh :: [String] -> IO ProcessResult 53 | runMmlh args = do 54 | Dir.createDirectoryIfMissing False "./tmp/" 55 | let env = [ (homePathEnvVarName, Just "./tmp/")] 56 | withArgs args 57 | $ withEnv env 58 | $ captureProcessResult Education.MakeMistakesToLearnHaskell.main 59 | 60 | shouldContainBS :: ByteString'.ByteString -> ByteString'.ByteString -> Expectation 61 | shouldContainBS a b = if ByteString'.isInfixOf b a 62 | then pure () 63 | else expectationFailure $ unwords [show a, "does not contain", show b] 64 | 65 | shouldExitWithHints :: [ByteString'.ByteString] -> ProcessResult -> IO () 66 | shouldExitWithHints hintMsgs (ProcessResult out err code ex) = do 67 | fmap show ex `shouldBe` Nothing 68 | err `shouldBe` ByteString'.empty 69 | mapM_ (out `shouldContainBS`) hintMsgs 70 | code `shouldBe` ExitFailure 1 71 | 72 | 73 | shouldVerifySuccess :: ProcessResult -> IO () 74 | shouldVerifySuccess (ProcessResult out err code ex) = do 75 | fmap show ex `shouldBe` Nothing 76 | err `shouldBe` ByteString'.empty 77 | out `shouldContainBS` "SUCCESS" 78 | code `shouldBe` ExitSuccess 79 | 80 | 81 | shouldPrintNotVerified :: ProcessResult -> IO () 82 | shouldPrintNotVerified (ProcessResult out err code ex) = do 83 | fmap show ex `shouldBe` Nothing 84 | err `shouldBe` ByteString'.empty 85 | out `shouldContainBS` "NOT VERIFIED" 86 | code `shouldBe` ExitSuccess 87 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | -------------------------------------------------------------------------------- /test/assets/1/error-messages/no-main.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\s\makeMistakesToLearnHaskell\test\assets\1\no-main.hs, S:\s\makeMistakesToLearnHaskell\test\assets\1\no-main.o ) 2 | 3 | S:\s\makeMistakesToLearnHaskell\test\assets\1\no-main.hs:1:1: error: 4 | Parse error: module header, import declaration 5 | or top-level declaration expected. 6 | | 7 | 1 | print "Hello, world!" 8 | | ^^^^^^^^^^^^^^^^^^^^^^^^ 9 | -------------------------------------------------------------------------------- /test/assets/1/error-messages/single-quote.txt: -------------------------------------------------------------------------------- 1 | 2 | test\assets\1\single-quote.hs:1:23: error: parse error on input ‘,’ 3 |  | 4 | 1 | main = putStrLn 'Hello, world!' 5 |  | ^ 6 | exit status 1 7 | -------------------------------------------------------------------------------- /test/assets/1/error-messages/typo.txt: -------------------------------------------------------------------------------- 1 | 2 | test\assets\1\typo.hs:1:8: error: 3 | • Variable not in scope: putStrL :: [Char] -> t 4 | • Perhaps you meant one of these: 5 | ‘putStr’ (imported from Prelude), 6 | ‘putStrLn’ (imported from Prelude) 7 |  | 8 | 1 | main = putStrL "Hello, world!" 9 |  | ^^^^^^^ 10 | exit status 1 11 | -------------------------------------------------------------------------------- /test/assets/1/no-main.hs: -------------------------------------------------------------------------------- 1 | putStrLn "Hello, world!" 2 | -------------------------------------------------------------------------------- /test/assets/1/single-quote-no-main.hs: -------------------------------------------------------------------------------- 1 | putStrLn 'Hello, world!' 2 | -------------------------------------------------------------------------------- /test/assets/1/single-quote.hs: -------------------------------------------------------------------------------- 1 | main = putStrLn 'Hello, world!' 2 | -------------------------------------------------------------------------------- /test/assets/1/typo.hs: -------------------------------------------------------------------------------- 1 | main = putStrL "Hello, world!" 2 | -------------------------------------------------------------------------------- /test/assets/2/error-messages/correct.txt: -------------------------------------------------------------------------------- 1 | 20.761245674740486 2 | -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-close-paren.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\s\makeMistakesToLearnHaskell\test\assets\2\no-close-paren.hs, S:\s\makeMistakesToLearnHaskell\test\assets\2\no-close-paren.o ) 2 | 3 | S:\s\makeMistakesToLearnHaskell\test\assets\2\no-close-paren.hs:2:1: error: 4 | parse error (possibly incorrect indentation or mismatched brackets) 5 | -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-main.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\s\makeMistakesToLearnHaskell\test\assets\2\no-main.hs, S:\s\makeMistakesToLearnHaskell\test\assets\2\no-main.o ) 2 | 3 | S:\s\makeMistakesToLearnHaskell\test\assets\2\no-main.hs:1:1: error: 4 | Parse error: module header, import declaration 5 | or top-level declaration expected. 6 | | 7 | 1 | print (60 / (1.7 * 1.7)) 8 | | ^^^^^^^^^^^^^^^^^^^^^^^^ 9 | -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-number-1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/2/error-messages/no-number-1.txt -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-number-2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/2/error-messages/no-number-2.txt -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-number-3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/2/error-messages/no-number-3.txt -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-open-paren.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/2/error-messages/no-open-paren.txt -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-paren.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/2/error-messages/no-paren.txt -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-slash.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/2/error-messages/no-slash.txt -------------------------------------------------------------------------------- /test/assets/2/error-messages/no-star.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/2/error-messages/no-star.txt -------------------------------------------------------------------------------- /test/assets/2/error-messages/template.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\s\makeMistakesToLearnHaskell\test\assets\2\template.hs, S:\s\makeMistakesToLearnHaskell\test\assets\2\template.o ) 2 | Linking S:\s\makeMistakesToLearnHaskell\test\assets\2\template.exe ... 3 | -------------------------------------------------------------------------------- /test/assets/2/error-messages/typo.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/2/error-messages/typo.txt -------------------------------------------------------------------------------- /test/assets/2/error-messages/wrong-number.txt: -------------------------------------------------------------------------------- 1 | 6.0 2 | -------------------------------------------------------------------------------- /test/assets/2/no-close-paren.hs: -------------------------------------------------------------------------------- 1 | main = print (60 / 1.7 * 1.7 2 | -------------------------------------------------------------------------------- /test/assets/2/no-main.hs: -------------------------------------------------------------------------------- 1 | print (60 / (1.7 * 1.7)) 2 | -------------------------------------------------------------------------------- /test/assets/2/no-number-1.hs: -------------------------------------------------------------------------------- 1 | main = print (/ (1.7 * 1.7)) 2 | -------------------------------------------------------------------------------- /test/assets/2/no-number-2.hs: -------------------------------------------------------------------------------- 1 | main = print (60 / ( * 1.7)) 2 | -------------------------------------------------------------------------------- /test/assets/2/no-number-3.hs: -------------------------------------------------------------------------------- 1 | main = print (60 / (1.7 *)) 2 | -------------------------------------------------------------------------------- /test/assets/2/no-open-paren.hs: -------------------------------------------------------------------------------- 1 | main = print 60 / (1.7 * 1.7)) 2 | -------------------------------------------------------------------------------- /test/assets/2/no-paren.hs: -------------------------------------------------------------------------------- 1 | main = print 60 / 1.7 * 1.7 2 | -------------------------------------------------------------------------------- /test/assets/2/no-slash.hs: -------------------------------------------------------------------------------- 1 | main = print (60 1.7 * 1.7) 2 | -------------------------------------------------------------------------------- /test/assets/2/no-star.hs: -------------------------------------------------------------------------------- 1 | main = print (60 / 1.7 1.7) 2 | -------------------------------------------------------------------------------- /test/assets/2/typo.hs: -------------------------------------------------------------------------------- 1 | main = prit (60 / (1.7 * 1.7)) 2 | -------------------------------------------------------------------------------- /test/assets/2/wrong-number.hs: -------------------------------------------------------------------------------- 1 | main = print (6 / 1.7 * 1.7) 2 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/correct.txt: -------------------------------------------------------------------------------- 1 | # # ####### # # ##### 2 | # # # # # # # 3 | # # # # # # # 4 | ####### ##### # # # # 5 | # # # # # # # 6 | # # # # # # # 7 | # # ####### ####### ####### ##### 8 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/incornsistent-indent.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\prj\makeMistakesToLearnHaskell\test\assets\3\incornsistent-indent.hs, S:\prj\makeMistakesToLearnHaskell\test\assets\3\incornsistent-indent.o ) 2 | 3 | S:\prj\makeMistakesToLearnHaskell\test\assets\3\incornsistent-indent.hs:2:3: error: 4 | • Couldn't match expected type ‘(String -> IO ()) 5 | -> [Char] -> IO a0’ 6 | with actual type ‘IO ()’ 7 | • The function ‘putStrLn’ is applied to three arguments, 8 | but its type ‘String -> IO ()’ has only one 9 | In a stmt of a 'do' block: 10 | putStrLn 11 | "# # ####### # # #####" 12 | putStrLn 13 | "# # # # # # #" 14 | In the expression: 15 | do putStrLn 16 | "# # ####### # # #####" 17 | putStrLn 18 | "# # # # # # #" 19 | putStrLn "# # # # # # #" 20 | putStrLn "####### ##### # # # #" 21 | putStrLn "# # # # # # #" 22 | .... 23 | | 24 | 2 | putStrLn "# # ####### # # #####" 25 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... 26 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/incornsistent-indent1.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\3\incornsistent-indent1.hs, test\assets\3\incornsistent-indent1.o ) 2 | 3 | test\assets\3\incornsistent-indent1.hs:2:3: error: 4 | • Couldn't match expected type ‘(String -> IO ()) 5 | -> [Char] -> IO a0’ 6 | with actual type ‘IO ()’ 7 | • The function ‘putStrLn’ is applied to three arguments, 8 | but its type ‘String -> IO ()’ has only one 9 | In a stmt of a 'do' block: 10 | putStrLn 11 | "# # ####### # # #####" 12 | putStrLn 13 | "# # # # # # #" 14 | In the expression: 15 | do putStrLn 16 | "# # ####### # # #####" 17 | putStrLn 18 | "# # # # # # #" 19 | putStrLn "# # # # # # #" 20 | putStrLn "####### ##### # # # #" 21 | putStrLn "# # # # # # #" 22 | .... 23 | | 24 | 2 | putStrLn "# # ####### # # #####" 25 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... 26 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/incornsistent-indent2.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\3\incornsistent-indent2.hs, test\assets\3\incornsistent-indent2.o ) 2 | 3 | test\assets\3\incornsistent-indent2.hs:3:3: error: 4 | parse error on input ‘putStrLn’ 5 | | 6 | 3 | putStrLn "# # ####### # # #####" 7 | | ^^^^^^^^ 8 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/no-do.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\prj\makeMistakesToLearnHaskell\test\assets\3\no-do.hs, S:\prj\makeMistakesToLearnHaskell\test\assets\3\no-do.o ) 2 | 3 | S:\prj\makeMistakesToLearnHaskell\test\assets\3\no-do.hs:2:3: error: 4 | • Couldn't match expected type ‘(String -> IO ()) 5 | -> [Char] 6 | -> (String -> IO ()) 7 | -> [Char] 8 | -> (String -> IO ()) 9 | -> [Char] 10 | -> (String -> IO ()) 11 | -> [Char] 12 | -> (String -> IO ()) 13 | -> [Char] 14 | -> (String -> IO ()) 15 | -> [Char] 16 | -> t’ 17 | with actual type ‘IO ()’ 18 | • The function ‘putStrLn’ is applied to 13 arguments, 19 | but its type ‘String -> IO ()’ has only one 20 | In the expression: 21 | putStrLn 22 | "# # ####### # # #####" 23 | putStrLn 24 | "# # # # # # #" 25 | putStrLn 26 | "# # # # # # #" 27 | putStrLn 28 | "####### ##### # # # #" 29 | putStrLn 30 | "# # # # # # #" 31 | putStrLn 32 | "# # # # # # #" 33 | putStrLn 34 | "# # ####### ####### ####### #####" 35 | In an equation for ‘main’: 36 | main 37 | = putStrLn 38 | "# # ####### # # #####" 39 | putStrLn 40 | "# # # # # # #" 41 | putStrLn 42 | "# # # # # # #" 43 | putStrLn 44 | "####### ##### # # # #" 45 | putStrLn 46 | "# # # # # # #" 47 | putStrLn 48 | "# # # # # # #" 49 | putStrLn 50 | "# # ####### ####### ####### #####" 51 | • Relevant bindings include 52 | main :: t 53 | (bound at S:\prj\makeMistakesToLearnHaskell\test\assets\3\no-do.hs:1:1) 54 | | 55 | 2 | putStrLn "# # ####### # # #####" 56 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... 57 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/no-main.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\s\makeMistakesToLearnHaskell\test\assets\3\no-main.hs, S:\s\makeMistakesToLearnHaskell\test\assets\3\no-main.o ) 2 | 3 | S:\s\makeMistakesToLearnHaskell\test\assets\3\no-main.hs:1:1: error: 4 | Parse error: module header, import declaration 5 | or top-level declaration expected. 6 | | 7 | 1 | putStrLn "# # ####### # # #####" 8 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 9 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/single-quote.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\prj\makeMistakesToLearnHaskell\test\assets\3\single-quote.hs, S:\prj\makeMistakesToLearnHaskell\test\assets\3\single-quote.o ) 2 | 3 | S:\prj\makeMistakesToLearnHaskell\test\assets\3\single-quote.hs:2:13: error: 4 | parse error on input ‘#’ 5 | | 6 | 2 | putStrLn '# # ####### # # #####' 7 | | ^ 8 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/typo.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\prj\makeMistakesToLearnHaskell\test\assets\3\typo.hs, S:\prj\makeMistakesToLearnHaskell\test\assets\3\typo.o ) 2 | 3 | S:\prj\makeMistakesToLearnHaskell\test\assets\3\typo.hs:3:3: error: 4 | • Variable not in scope: putStrL :: [Char] -> IO a0 5 | • Perhaps you meant one of these: 6 | ‘putStrLn’ (imported from Prelude), 7 | ‘putStr’ (imported from Prelude) 8 | | 9 | 3 | putStrL "# # # # # # #" 10 | | ^^^^^^^ 11 | -------------------------------------------------------------------------------- /test/assets/3/error-messages/wrong-output.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( S:\prj\makeMistakesToLearnHaskell\test\assets\3\wrong-output.hs, S:\prj\makeMistakesToLearnHaskell\test\assets\3\wrong-output.o ) 2 | Linking S:\prj\makeMistakesToLearnHaskell\test\assets\3\wrong-output.exe ... 3 | -------------------------------------------------------------------------------- /test/assets/3/incornsistent-indent.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | putStrLn "# # ####### # # #####" 3 | putStrLn "# # # # # # #" 4 | putStrLn "# # # # # # #" 5 | putStrLn "####### ##### # # # #" 6 | putStrLn "# # # # # # #" 7 | putStrLn "# # # # # # #" 8 | putStrLn "# # ####### ####### ####### #####" 9 | -------------------------------------------------------------------------------- /test/assets/3/incornsistent-indent1.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | putStrLn "# # ####### # # #####" 3 | putStrLn "# # # # # # #" 4 | putStrLn "# # # # # # #" 5 | putStrLn "####### ##### # # # #" 6 | putStrLn "# # # # # # #" 7 | putStrLn "# # # # # # #" 8 | putStrLn "# # ####### ####### ####### #####" 9 | -------------------------------------------------------------------------------- /test/assets/3/incornsistent-indent2.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | putStrLn "# # # # # # #" 3 | putStrLn "# # ####### # # #####" 4 | putStrLn "# # # # # # #" 5 | putStrLn "####### ##### # # # #" 6 | putStrLn "# # # # # # #" 7 | putStrLn "# # # # # # #" 8 | putStrLn "# # ####### ####### ####### #####" 9 | -------------------------------------------------------------------------------- /test/assets/3/no-do.hs: -------------------------------------------------------------------------------- 1 | main = 2 | putStrLn "# # ####### # # #####" 3 | putStrLn "# # # # # # #" 4 | putStrLn "# # # # # # #" 5 | putStrLn "####### ##### # # # #" 6 | putStrLn "# # # # # # #" 7 | putStrLn "# # # # # # #" 8 | putStrLn "# # ####### ####### ####### #####" 9 | -------------------------------------------------------------------------------- /test/assets/3/no-main.hs: -------------------------------------------------------------------------------- 1 | putStrLn "# # ####### # # #####" 2 | putStrLn "# # # # # # #" 3 | putStrLn "# # # # # # #" 4 | putStrLn "####### ##### # # # #" 5 | putStrLn "# # # # # # #" 6 | putStrLn "# # # # # # #" 7 | putStrLn "# # ####### ####### ####### #####" 8 | -------------------------------------------------------------------------------- /test/assets/3/single-quote.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | putStrLn '# # ####### # # #####' 3 | putStrLn '# # # # # # #' 4 | putStrLn '# # # # # # #' 5 | putStrLn '####### ##### # # # #' 6 | putStrLn '# # # # # # #' 7 | putStrLn '# # # # # # #' 8 | putStrLn '# # ####### ####### ####### #####' 9 | -------------------------------------------------------------------------------- /test/assets/3/typo.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | putStrLn "# # ####### # # #####" 3 | putStrL "# # # # # # #" 4 | putStrLn "# # # # # # #" 5 | putStrLn "####### ##### # # # #" 6 | putStrLn "# # # # # # #" 7 | putStrLn "# # # # # # #" 8 | putStrLn "# # ####### ####### ####### #####" 9 | -------------------------------------------------------------------------------- /test/assets/3/wrong-output.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | putStrLn "# # ####### # # #####" 3 | putStrLn "# # # # # # #" 4 | putStrLn "# # # # # # #" 5 | putStrLn "####### ##### # # # #" 6 | putStrLn "# # # # # # #" 7 | putStrLn "# # # # # # #" 8 | -------------------------------------------------------------------------------- /test/assets/4/equal-no-space.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input=getContents 3 | putStr (unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/equal.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input = getContents 3 | putStr (unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/equal-no-space.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\equal-no-space.hs, test\assets\4\equal-no-space.o ) 2 | 3 | test\assets\4\equal-no-space.hs:2:8: error: 4 | parse error on input ‘=’ 5 | Perhaps you need a 'let' in a 'do' block? 6 | e.g. 'let x = 5' instead of 'x = 5' 7 | | 8 | 2 | input=getContents 9 | | ^ 10 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/equal.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\equal.hs, test\assets\4\equal.o ) 2 | 3 | test\assets\4\equal.hs:2:9: error: 4 | parse error on input ‘=’ 5 | Perhaps you need a 'let' in a 'do' block? 6 | e.g. 'let x = 5' instead of 'x = 5' 7 | | 8 | 2 | input = getContents 9 | | ^ 10 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/incornsistent-indent1.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\incornsistent-indent1.hs, test\assets\4\incornsistent-indent1.o ) 2 | 3 | test\assets\4\incornsistent-indent1.hs:2:2: error: 4 | The last statement in a 'do' block must be an expression 5 | input <- getContents putStr (unlines (reverse (lines input))) 6 | | 7 | 2 | input <- getContents 8 | | ^^^^^^^^^^^^^^^^^^^^... 9 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/incornsistent-indent2.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\incornsistent-indent2.hs, test\assets\4\incornsistent-indent2.o ) 2 | 3 | test\assets\4\incornsistent-indent2.hs:3:2: error: 4 | parse error on input ‘putStr’ 5 | | 6 | 3 | putStr (unlines (reverse (lines input))) 7 | | ^^^^^^ 8 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/main-arrow.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\main-arrow.hs, test\assets\4\main-arrow.o ) 2 | 3 | test\assets\4\main-arrow.hs:1:6: error: 4 | parse error on input ‘<-’ 5 | Perhaps this statement should be within a 'do' block? 6 | | 7 | 1 | main <- do 8 | | ^^ 9 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-arrow.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-arrow.hs, test\assets\4\no-arrow.o ) 2 | 3 | test\assets\4\no-arrow.hs:2:35: error: 4 | • Couldn't match type ‘IO String’ with ‘[Char]’ 5 | Expected type: String 6 | Actual type: IO String 7 | • In the first argument of ‘lines’, namely ‘getContents’ 8 | In the first argument of ‘reverse’, namely ‘(lines getContents)’ 9 | In the first argument of ‘unlines’, namely 10 | ‘(reverse (lines getContents))’ 11 | | 12 | 2 | putStr (unlines (reverse (lines getContents))) 13 | | ^^^^^^^^^^^ 14 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-close-paren.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-close-paren.hs, test\assets\4\no-close-paren.o ) 2 | 3 | test\assets\4\no-close-paren.hs:4:1: error: 4 | parse error (possibly incorrect indentation or mismatched brackets) 5 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-do.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-do.hs, test\assets\4\no-do.o ) 2 | 3 | test\assets\4\no-do.hs:2:9: error: 4 | parse error on input ‘<-’ 5 | Perhaps this statement should be within a 'do' block? 6 | | 7 | 2 | input <- getContents 8 | | ^^ 9 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-main.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-main.hs, test\assets\4\no-main.o ) 2 | 3 | test\assets\4\no-main.hs:1:7: error: 4 | parse error on input ‘<-’ 5 | Perhaps this statement should be within a 'do' block? 6 | | 7 | 1 | input <- getContents 8 | | ^^ 9 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-open-paren1.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-open-paren1.hs, test\assets\4\no-open-paren1.o ) 2 | 3 | test\assets\4\no-open-paren1.hs:3:41: error: 4 | parse error on input ‘)’ 5 | | 6 | 3 | putStr unlines (reverse (lines input))) 7 | | ^ 8 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-open-paren2.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-open-paren2.hs, test\assets\4\no-open-paren2.o ) 2 | 3 | test\assets\4\no-open-paren2.hs:3:41: error: 4 | parse error on input ‘)’ 5 | | 6 | 3 | putStr (unlines reverse (lines input))) 7 | | ^ 8 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-open-paren3.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-open-paren3.hs, test\assets\4\no-open-paren3.o ) 2 | 3 | test\assets\4\no-open-paren3.hs:3:41: error: 4 | parse error on input ‘)’ 5 | | 6 | 3 | putStr (unlines (reverse lines input))) 7 | | ^ 8 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-paren.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-paren.hs, test\assets\4\no-paren.o ) 2 | 3 | test\assets\4\no-paren.hs:3:3: error: 4 | • Couldn't match expected type ‘([a0] -> [a0]) 5 | -> (String -> [String]) -> String -> IO b’ 6 | with actual type ‘IO ()’ 7 | • The function ‘putStr’ is applied to four arguments, 8 | but its type ‘String -> IO ()’ has only one 9 | In a stmt of a 'do' block: putStr unlines reverse lines input 10 | In the expression: 11 | do input <- getContents 12 | putStr unlines reverse lines input 13 | • Relevant bindings include 14 | main :: IO b (bound at test\assets\4\no-paren.hs:1:1) 15 | | 16 | 3 | putStr unlines reverse lines input 17 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 | 19 | test\assets\4\no-paren.hs:3:10: error: 20 | • Couldn't match type ‘[String] -> String’ with ‘[Char]’ 21 | Expected type: String 22 | Actual type: [String] -> String 23 | • Probable cause: ‘unlines’ is applied to too few arguments 24 | In the first argument of ‘putStr’, namely ‘unlines’ 25 | In a stmt of a 'do' block: putStr unlines reverse lines input 26 | In the expression: 27 | do input <- getContents 28 | putStr unlines reverse lines input 29 | | 30 | 3 | putStr unlines reverse lines input 31 | | ^^^^^^^ 32 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-paren1.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-paren1.hs, test\assets\4\no-paren1.o ) 2 | 3 | test\assets\4\no-paren1.hs:3:3: error: 4 | • Couldn't match expected type ‘[String] -> IO b’ 5 | with actual type ‘IO ()’ 6 | • The function ‘putStr’ is applied to two arguments, 7 | but its type ‘String -> IO ()’ has only one 8 | In a stmt of a 'do' block: putStr unlines (reverse (lines input)) 9 | In the expression: 10 | do input <- getContents 11 | putStr unlines (reverse (lines input)) 12 | • Relevant bindings include 13 | main :: IO b (bound at test\assets\4\no-paren1.hs:1:1) 14 | | 15 | 3 | putStr unlines (reverse (lines input)) 16 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 17 | 18 | test\assets\4\no-paren1.hs:3:10: error: 19 | • Couldn't match type ‘[String] -> String’ with ‘[Char]’ 20 | Expected type: String 21 | Actual type: [String] -> String 22 | • Probable cause: ‘unlines’ is applied to too few arguments 23 | In the first argument of ‘putStr’, namely ‘unlines’ 24 | In a stmt of a 'do' block: putStr unlines (reverse (lines input)) 25 | In the expression: 26 | do input <- getContents 27 | putStr unlines (reverse (lines input)) 28 | | 29 | 3 | putStr unlines (reverse (lines input)) 30 | | ^^^^^^^ 31 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-paren2.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-paren2.hs, test\assets\4\no-paren2.o ) 2 | 3 | test\assets\4\no-paren2.hs:3:11: error: 4 | • Couldn't match expected type ‘[String] -> String’ 5 | with actual type ‘[Char]’ 6 | • The function ‘unlines’ is applied to two arguments, 7 | but its type ‘[String] -> [Char]’ has only one 8 | In the first argument of ‘putStr’, namely 9 | ‘(unlines reverse (lines input))’ 10 | In a stmt of a 'do' block: putStr (unlines reverse (lines input)) 11 | | 12 | 3 | putStr (unlines reverse (lines input)) 13 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 14 | 15 | test\assets\4\no-paren2.hs:3:19: error: 16 | • Couldn't match expected type ‘[String]’ 17 | with actual type ‘[a0] -> [a0]’ 18 | • Probable cause: ‘reverse’ is applied to too few arguments 19 | In the first argument of ‘unlines’, namely ‘reverse’ 20 | In the first argument of ‘putStr’, namely 21 | ‘(unlines reverse (lines input))’ 22 | In a stmt of a 'do' block: putStr (unlines reverse (lines input)) 23 | | 24 | 3 | putStr (unlines reverse (lines input)) 25 | | ^^^^^^^ 26 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/no-paren3.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\no-paren3.hs, test\assets\4\no-paren3.o ) 2 | 3 | test\assets\4\no-paren3.hs:3:20: error: 4 | • Couldn't match expected type ‘String -> [String]’ 5 | with actual type ‘[a0]’ 6 | • The function ‘reverse’ is applied to two arguments, 7 | but its type ‘[a0] -> [a0]’ has only one 8 | In the first argument of ‘unlines’, namely ‘(reverse lines input)’ 9 | In the first argument of ‘putStr’, namely 10 | ‘(unlines (reverse lines input))’ 11 | | 12 | 3 | putStr (unlines (reverse lines input)) 13 | | ^^^^^^^^^^^^^^^^^^^ 14 | 15 | test\assets\4\no-paren3.hs:3:28: error: 16 | • Couldn't match expected type ‘[a0]’ 17 | with actual type ‘String -> [String]’ 18 | • Probable cause: ‘lines’ is applied to too few arguments 19 | In the first argument of ‘reverse’, namely ‘lines’ 20 | In the first argument of ‘unlines’, namely ‘(reverse lines input)’ 21 | In the first argument of ‘putStr’, namely 22 | ‘(unlines (reverse lines input))’ 23 | | 24 | 3 | putStr (unlines (reverse lines input)) 25 | | ^^^^^ 26 | -------------------------------------------------------------------------------- /test/assets/4/error-messages/wrong-output.txt: -------------------------------------------------------------------------------- 1 | [1 of 1] Compiling Main ( test\assets\4\wrong-output.hs, test\assets\4\wrong-output.o ) 2 | Linking test\assets\4\wrong-output.exe ... 3 | -------------------------------------------------------------------------------- /test/assets/4/incornsistent-indent1.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr (unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/incornsistent-indent2.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr (unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/main-arrow.hs: -------------------------------------------------------------------------------- 1 | main <- do 2 | input <- getContents 3 | putStr (unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/no-arrow.hs: -------------------------------------------------------------------------------- 1 | main = 2 | putStr (unlines (reverse (lines getContents))) 3 | -------------------------------------------------------------------------------- /test/assets/4/no-close-paren.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr (unlines (reverse (lines input)) 4 | -------------------------------------------------------------------------------- /test/assets/4/no-do.hs: -------------------------------------------------------------------------------- 1 | main = 2 | input <- getContents 3 | putStr (unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/no-main.hs: -------------------------------------------------------------------------------- 1 | input <- getContents 2 | putStr (unlines (reverse (lines input))) 3 | -------------------------------------------------------------------------------- /test/assets/4/no-open-paren1.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/no-open-paren2.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr (unlines reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/no-open-paren3.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr (unlines (reverse lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/4/no-paren.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr unlines reverse lines input 4 | -------------------------------------------------------------------------------- /test/assets/4/no-paren1.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr unlines (reverse (lines input)) 4 | -------------------------------------------------------------------------------- /test/assets/4/no-paren2.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr (unlines reverse (lines input)) 4 | -------------------------------------------------------------------------------- /test/assets/4/no-paren3.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStr (unlines (reverse lines input)) 4 | -------------------------------------------------------------------------------- /test/assets/4/wrong-output.hs: -------------------------------------------------------------------------------- 1 | main = do 2 | input <- getContents 3 | putStrLn (unlines (reverse (lines input))) 4 | -------------------------------------------------------------------------------- /test/assets/common/empty.hs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell-jp/makeMistakesToLearnHaskell/c7cda4384b88a37e1f279ddc8606ae9c41c1ec44/test/assets/common/empty.hs --------------------------------------------------------------------------------