+
Haskell-jpなので基本的に日本語の記事を公開しましょう。
+-
+
- マークダウンを使えます +
- 脚注 +
Link to
+hereマークダウンを使えます
+Pandocのマークダウンを使えるので、脚注なども1このようにいれられます。
+Link to
+here脚注
++
-
+
ただの脚注です。 +テスト以外の何物でもありません。↩︎
+
diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 544f4a51..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,86 +0,0 @@ -version: 2 -jobs: - build: - # Ref: https://mmhaskell.com/blog/2018/4/25/dockerizing-our-haskell-app - docker: - - image: haskell:9.0.2-slim - steps: - - run: apt update - - run: apt install -y zip jq curl - - run: stack upgrade - - run: "echo 'tcp 6 TCP' > /etc/protocols" - - run: "stack config --system-ghc set system-ghc --global true" - - checkout - - - restore_cache: - keys: - - 'dependencies-{{ checksum "stack.yaml" }}-{{ checksum "haskell-jp-blog.cabal" }}' - - 'dependencies-' - - run: stack build --compiler=ghc-9.0.2 --no-terminal --only-dependencies - - save_cache: - key: 'dependencies-{{ checksum "stack.yaml" }}-{{ checksum "haskell-jp-blog.cabal" }}' - paths: - - ~/.stack/ - - .stack-work/ - - - restore_cache: - keys: - - 'executable-{{ checksum "src/site.hs" }}' - - 'executable-' - - run: stack --compiler=ghc-9.0.2 --local-bin-path='.' --no-terminal install --pedantic - - save_cache: - key: 'executable-{{ checksum "src/site.hs" }}' - paths: - - ./site - - - run: ./site build - - store_artifacts: { path: ./generated-site/ } - - - run: | - if [ "$CIRCLE_BRANCH" != master ] ; then - printenv | - grep -E '^CIRCLE_|^HOME' | # Circle CIの環境変数を抽出して、preview botのサーバーにJSONとして渡す https://circleci.com/docs/2.0/env-vars/ - jq -c -s -R 'split("\n") - | map (split("=") - | select(.[0] != null) - | {(.[0]): .[1:] | join("=")}) - | add' | - curl -H 'Content-Type:application/json' -d @- \ - http://haskell-jp-blog-artifact.hask.moe/ - fi - - persist_to_workspace: - root: . - paths: - - ./generated-site - - deploy: - docker: - - image: haskell:9.0.2-slim - steps: - - checkout: - path: ~/project - - add-ssh-keys: - fingerprints: - - "9f:97:4e:99:72:c0:62:1d:db:9e:8e:ce:62:3f:0a:52" - - run: apt update - - run: apt install -y make ssh-client - - attach_workspace: - at: . - - run: | - git config --global user.email "ci@haskell.jp" - git config --global user.name "Circle CI User" - ssh-keyscan github.com >> ~/.ssh/known_hosts - STACK_LOCAL_INSTALL_PATH=dummy make -W site -W dummy/site -e deploy - -workflows: - version: 2 - build_and_deploy: - jobs: - - build - - deploy: - requires: - - build - filters: - branches: - only: - - master diff --git a/.github/ISSUE_TEMPLATE/topic-request.md b/.github/ISSUE_TEMPLATE/topic-request.md deleted file mode 100644 index 6806cf48..00000000 --- a/.github/ISSUE_TEMPLATE/topic-request.md +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 86498bf4..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/.gitignore b/.gitignore index ff2adfa8..769b0c12 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ cabal-dev *.chi *.chs.h .virtualenv -*.exe .hsenv .cabal-sandbox/ cabal.sandbox.config @@ -17,5 +16,3 @@ cabal.config # stack .stack-work/ - -*~ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..631eb600 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,58 @@ + +# Use new container infrastructure to enable caching +sudo: false + +# Choose a lightweight base image; we provide our own build tools. +language: node_js + +# Caching so the next build will be fast too. +cache: + directories: + - $HOME/.ghc + - $HOME/.cabal + - $HOME/.stack + +addons: + apt: + packages: + - ghc-8.0.2 + - libgmp-dev + sources: + - hvr-ghc + +# Use the latest version of node-6 available +node_js: "6" + +before_install: +# Using compiler above sets CC to an invalid value, so unset it +- unset CC + +# Download and unpack the stack executable +- export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$HOME/.local/bin:/opt/alex/$ALEXVER/bin:/opt/happy/$HAPPYVER/bin:$HOME/.cabal/bin:$PATH +- mkdir -p ~/.local/bin +- | + travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' + + # Use the more reliable S3 mirror of Hackage + mkdir -p $HOME/.cabal + echo 'remote-repo: hackage.haskell.org:http://hackage.fpcomplete.com/' > $HOME/.cabal/config + echo 'remote-repo-cache: $HOME/.cabal/packages' >> $HOME/.cabal/config + + if [ "$CABALVER" != "1.16" ] + then + echo 'jobs: $ncpus' >> $HOME/.cabal/config + fi + +install: +- if [ -f configure.ac ]; then autoreconf -i; fi +- | + set -ex + time stack --no-terminal --install-ghc test --bench --only-dependencies + set +ex + +script: +- | + set -ex + time travis_wait 30 stack --no-terminal test --bench --no-run-benchmarks --haddock --no-haddock-deps + time stack exec -- site build + set +ex diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 960c0975..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,38 +0,0 @@ -# 記事を寄稿する方法 - -Haskell-jp Blogの寄稿に興味を持っていただいてありがとうございます! -記事を寄稿していただける場合、下記の流れで行います。 - -1. 投稿したい人が、[preprocessed-site/posts/](https://github.com/haskell-jp/blog/tree/master/preprocessed-site/posts/)というディレクトリーに、Markdownで記事を書いて置いてください。 - - GitHubのアカウントをお持ちであれば、上記のリンク先にある、"Create new file"というボタンから追加できるはずです。 - - 記事の先頭に書く内容などは、同じディレクトリーにある、適当なほかの記事を参考にしてください。 - - 内部でPandocを使用しているので、[Pandocがサポートしている構文](http://pandoc.org/MANUAL.html#pandocs-markdown)であれば、すべて利用できます。 -1. 作成した記事を含めたコミットで、Pull requestを送ってください。先ほどの"Create new file"というボタンからの導線に従えば、割と簡単にできるはずです。 -1. [GitHubのHaskell-jp organization](https://github.com/haskell-jp)に所属する人などが、記事をレビューします。適宜対応してください。 -1. 送ったPull requestがマージされると、CIが自動で記事を公開してくれます! - -# 記事のライセンスについて - -原則として、次のルールが適用されます。 - -- 寄稿者が執筆した記事の著作権は、**寄稿者のもの**となります。 -- その上で、寄稿者が執筆した記事に対しては、**「[クリエイティブ・コモンズ 表示 4.0 国際 ライセンス](https://creativecommons.org/licenses/by/4.0/)(通称CC-BY 4.0)」**が適用されます。 - - 従って、Haskell-jp Blogに公開される記事は、寄稿者以外の人が、寄稿者の名前を表示させた上で、自由に再配布したり、改変したりすることができるという点を、あらかじめご了承ください。 - - 詳細は[クリエイティブ・コモンズ 表示 4.0 国際 ライセンスの条文](https://creativecommons.org/licenses/by/4.0/legalcode.ja)をご覧ください。 -- ただし、寄稿者以外の人がGitHubのPull requestやIssue報告などを通じて寄稿者の記事を修正する場合、著作権は、**記事の著作者の同意の下、記事の寄稿者に委譲**するものとします。Pull requestを送った人や、Issueを報告した人のものとはなりません。 - -もし記事のライセンスについて、何かしら特別な事情がある場合、GitHubのIssueを通じてご相談ください。例外的な対応も、適宜検討します。 - -# 記事にして欲しい内容を提案する方法と注意点 - -記事の寄稿ではなく、記事にして欲しい内容を提案していただける場合は、[このリンク](https://github.com/haskell-jp/blog/issues/new?template=topic-request.md&labels=Topic+Request)より Issue を作成してください。 -Issue には、どのような記事を書いてほしいか書いてください。 -例えば: - -- ○○パッケージの使い方やサンプルが知りたい -- 数学用語と Haskell 用語の対応関係が知りたい -- 少し古めの Haskell 本を読む上での注意点が知りたい -- などなど - -**ただし、知見の持ち主が居ないかもしれませんし、誰かの負担になるものなので必ず記事になるとは限りません。** -また、場合によっては既に記事があるため、既存の記事を薦められるかもしれません。 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 2d8dcd67..00000000 --- a/LICENSE +++ /dev/null @@ -1,44 +0,0 @@ -Copyright (c) 2017 Haskell-jp - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -This project is forked from [arow-oss/blog](https://github.com/arow-oss/blog). -Here is the original copyright notice for it. - -Copyright (c) 2017-2017 ARoW - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index f13d14b1..00000000 --- a/Makefile +++ /dev/null @@ -1,110 +0,0 @@ -.PHONY: build clean deploy release site watch -all: site - -############# -# Variables # -############# - -# Path to the directory that `stack` uses to install binaries locally. -STACK_LOCAL_INSTALL_PATH ?= $(shell stack path --local-install-root) - -# Path to the `site` binary. -SITE_PROG_PATH = $(STACK_LOCAL_INSTALL_PATH)/site - -# The current commit's git hash. -GIT_HASH = $(shell git rev-parse --short HEAD) - -################################ -## Targets for specific files ## -################################ - -# target for the `site` binary. This binary is used to actually create the -# html files. -$(SITE_PROG_PATH): src/site.hs - @echo "Building..." - @stack build - @echo "Built." - -##################### -## General targets ## -##################### - -# Build the `site` binary. The `site` binary is used to build the actual .html -# files for the site. -build: $(SITE_PROG_PATH) - -# Clean all generated files. -clean: - @echo "Cleaning..." - -@stack exec -- site clean 2>/dev/null || true - @rm -rf .hakyll-cache/ generated-site/ - @stack clean - @echo "Clean." - -# Deploy the site. -# Commit the generated-site directory to the gh-pages git branch. -# The way this is done is pretty hacky, but it works. -deploy: site - # Make sure this temporary working directory is empty. - # (TODO: Really we should be using a directory with a random filename, - # generated with something like mktemp.) - rm -rf /tmp/haskell-jp-blog-deploy/ - mkdir /tmp/haskell-jp-blog-deploy/ - # Copy the generated site to the temp directory. - cp -r generated-site /tmp/haskell-jp-blog-deploy/ - # Checkout the gh-pages branch. -ifdef GITHUB_TOKEN - git remote set-url origin "https://${GITHUB_TOKEN}@github.com/haskell-jp/blog.git" - git fetch origin gh-pages - git checkout -b gh-pages FETCH_HEAD -else - git checkout -t origin/gh-pages -endif - # Remove the pages for the current site. - git rm -r -f --ignore-unmatch * - git status - # Copy all of the generated site's files to the current directory. - cp -r /tmp/haskell-jp-blog-deploy/generated-site/* ./ - - # Disable Jekyll to serve files whose names begin with `_`. - # https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/ - touch ./.nojekyll - - # Add everything back. (A lot of files probably won't change, so, for - # instance, they won't show up on 'git status' even though we just did 'git - # rm -rf *'. A 'git rm -rf FILE' followed by 'git add FILE' is a noop if - # the file hasn't changed.) - git add -A . - git status - # Do the commit and push. - @git diff --exit-code; \ - rc=$?; if [ $rc != 0 ] ; then \ - git commit -m "Release $(GIT_HASH) on `date`."; \ - git push -f origin gh-pages; \ - else \ - echo "Skip commit and push to gh-pages"; \ - fi -ifndef GITHUB_TOKEN - # Go back to master. - git checkout master -endif - rm -rf /tmp/haskell-jp-blog-deploy - -# Alias for deploy. -release: deploy - -# Generate the .html files for our blog. -site: $(SITE_PROG_PATH) - @# We don't actually need to use rebuild here, we could just use build. - @# If this blog becomes really big and produces tons of pages, then switching - @# to 'build' here (and adding an additional site-rebuild target) would be a - @# good idea. - stack exec -- site rebuild - -test: - stack test - -# Run a test webserver on http://0.0.0.0:8000 serving up the content of our -# blog. If the content changes, it is automatically rebuilt. -watch: $(SITE_PROG_PATH) - stack exec -- site watch --host 0.0.0.0 diff --git a/README.md b/README.md deleted file mode 100644 index 4b61cf26..00000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# [Haskell-jp Blog](https://haskell.jp/blog) - -[](http://travis-ci.org/haskell-jp/blog) -[](https://opensource.org/licenses/MIT) - -このリポジトリーでは[Haskell-jp Blog](https://haskell.jp/blog)の記事や、記事の内容などについての問題を管理しています。 -広くHaskellに関する記事や、読みたい記事のテーマを常に募集しています! -寄稿方法等は下記をご覧ください。 - -# 記事を投稿したい場合 - -Haskell-jp Blogの寄稿に興味を持っていただいてありがとうございます! -基本的には投稿する人は、**Markdownで記事を書いて、Pull requestを送るだけ**です! -「記事を書いてみたいけど、ネタがない...」という場合、[こちらの一覧](https://github.com/haskell-jp/blog/issues?q=is%3Aopen+is%3Aissue+label%3A%22Topic+Request%22)を覗いてみてください。あなたが書きたい話題があるかもしれません! - -詳しい方法や、あなたが書いた記事に適用されるライセンスについては[CONTRIBUTING.md](./CONTRIBUTING.md)をご覧ください。 - -# 「こんなテーマの記事を読みたい!」場合 - -Haskell-jp Blogでは、「Haskellについて、こんなことを知りたい!」という記事のテーマも常に募集しています! -すでに知りたいテーマが[こちら](https://github.com/haskell-jp/blog/issues?q=is%3Aopen+is%3Aissue+label%3A%22Topic+Request%22)に登録されていた場合、 :+1: を押して支援するのもいいでしょう! -詳細は[CONTRIBUTING.mdのこちらのセクション](CONTRIBUTING.md#%E8%A8%98%E4%BA%8B%E3%81%AB%E3%81%97%E3%81%A6%E6%AC%B2%E3%81%97%E3%81%84%E5%86%85%E5%AE%B9%E3%82%92%E6%8F%90%E6%A1%88%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95%E3%81%A8%E6%B3%A8%E6%84%8F%E7%82%B9)をご覧ください。 - -# 記事の内容などについて、問題を報告したい場合 - -その他、記事の内容や、ウェブサイトの構成などについての問題があった場合、[GitHubのissue](https://github.com/haskell-jp/blog/issues/new)でご連絡ください。 -なお、既存の記事を修正される場合、修正内容についての著作権は、原則として、記事を元の寄稿者のものとなるのでご注意ください。 -詳細は、[CONTRIBUTING.mdのライセンスについての注記](./CONTRIBUTING.md#記事のライセンスについて)をご覧ください。 diff --git a/Setup.hs b/Setup.hs deleted file mode 100644 index 9a994af6..00000000 --- a/Setup.hs +++ /dev/null @@ -1,2 +0,0 @@ -import Distribution.Simple -main = defaultMain diff --git a/css/bootstrap.css b/css/bootstrap.css new file mode 100644 index 00000000..afc015af --- /dev/null +++ b/css/bootstrap.css @@ -0,0 +1 @@ +html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}@media print{*,*:before,*:after{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{position:static;display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"]{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.form-group-sm .form-control{height:30px;line-height:30px}textarea.form-group-sm .form-control,select[multiple].form-group-sm .form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:5px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.form-group-lg .form-control{height:46px;line-height:46px}textarea.form-group-lg .form-control,select[multiple].form-group-lg .form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:10px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.333333px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default.focus,.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:hover,.btn-primary:focus,.btn-primary.focus,.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success.focus,.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info.focus,.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning.focus,.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger.focus,.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:normal;color:#337ab7;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px 15px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding:48px 0}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:transparent;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.42857143px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-weight:normal;line-height:1.4;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-weight:normal;line-height:1.42857143;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;perspective:1000}.carousel-inner>.item.next,.carousel-inner>.item.active.right{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0%,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0%,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0%,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0%,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0%,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0%,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/css/clean-blog.css b/css/clean-blog.css new file mode 100644 index 00000000..76144710 --- /dev/null +++ b/css/clean-blog.css @@ -0,0 +1 @@ +body{font-family:'Lora','Times New Roman',serif;font-size:20px;color:#404040}p{line-height:1.5;margin:30px 0}p a{text-decoration:underline}h1,h2,h3,h4,h5,h6{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:800}a{color:#404040;text-decoration:underline}a:hover,a:focus{color:#d66c79}a img:hover,a img:focus{cursor:zoom-in}blockquote{color:#808080;font-style:italic}hr.small{max-width:100px;margin:15px auto;border-width:4px;border-color:#333}.navbar-custom{position:absolute;top:0;left:0;width:100%;z-index:3;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}.navbar-custom .navbar-brand{font-weight:800}.navbar-custom .nav li a{text-transform:uppercase;font-size:12px;font-weight:800;letter-spacing:1px;text-decoration:none}@media only screen and (min-width:768px){.navbar-custom{background:transparent;border-bottom:1px solid transparent}.navbar-custom .navbar-brand{color:white;padding:20px}.navbar-custom .navbar-brand:hover,.navbar-custom .navbar-brand:focus{color:rgba(255,255,255,0.8)}.navbar-custom .nav li a{color:white;padding:20px}.navbar-custom .nav li a:hover,.navbar-custom .nav li a:focus{color:rgba(255,255,255,0.8)}}@media only screen and (min-width:1170px){.navbar-custom{-webkit-transition:background-color 0.3s;-moz-transition:background-color 0.3s;transition:background-color 0.3s;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.navbar-custom.is-fixed{position:fixed;top:-61px;background-color:rgba(255,255,255,0.9);border-bottom:1px solid #f2f2f2;-webkit-transition:-webkit-transform 0.3s;-moz-transition:-moz-transform 0.3s;transition:transform 0.3s}.navbar-custom.is-fixed .navbar-brand{color:#404040}.navbar-custom.is-fixed .navbar-brand:hover,.navbar-custom.is-fixed .navbar-brand:focus{color:#0085a1}.navbar-custom.is-fixed .nav li a{color:#404040}.navbar-custom.is-fixed .nav li a:hover,.navbar-custom.is-fixed .nav li a:focus{color:#0085a1}.navbar-custom.is-visible{-webkit-transform:translate3d(0,100%,0);-moz-transform:translate3d(0,100%,0);-ms-transform:translate3d(0,100%,0);-o-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.intro-header{background-color:#808080;background:no-repeat center center;background-attachment:scroll;-webkit-background-size:cover;-moz-background-size:cover;background-size:cover;-o-background-size:cover;margin-bottom:50px}.intro-header .site-heading,.intro-header .post-heading,.intro-header .page-heading{padding:100px 0 50px;color:white}@media only screen and (min-width:768px){.intro-header .site-heading,.intro-header .post-heading,.intro-header .page-heading{padding:150px 0}}.intro-header .site-heading,.intro-header .page-heading{text-align:center}.intro-header .site-heading h1,.intro-header .page-heading h1{margin-top:0;font-size:50px}.intro-header .site-heading .subheading,.intro-header .page-heading .subheading{font-size:24px;line-height:1.1;display:block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:300;margin:10px 0 0}.intro-header .site-heading .subheading,.intro-header .post-heading .subheading,.intro-header .post-heading .meta{color:#333}.intro-header .site-heading h1,.intro-header .post-heading h1{color:#333}.navbar-brand img:hover{cursor:pointer}@media only screen and (min-width:768px){.intro-header .site-heading h1,.intro-header .page-heading h1{font-size:80px}}.intro-header .post-heading h1{font-size:35px}.intro-header .post-heading .subheading,.intro-header .post-heading .meta{line-height:1.1;display:block}.intro-header .post-heading .subheading{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:24px;margin:10px 0 30px;font-weight:600}.intro-header .post-heading .meta{font-family:'Lora','Times New Roman',serif;font-style:italic;font-weight:300;font-size:20px}.intro-header .post-heading .meta a{color:#333}@media only screen and (min-width:768px){.intro-header .post-heading h1{font-size:55px}.intro-header .post-heading .subheading{font-size:30px}}.post-preview>a{color:#404040;text-decoration:none}.post-preview>a:hover,.post-preview>a:focus{text-decoration:none;color:#d66c79}.post-preview>a>.post-title{font-size:30px;margin-top:30px;margin-bottom:10px}.post-preview>a>.post-subtitle{margin:0;font-weight:300;margin-bottom:10px}.post-preview>.post-meta{color:#808080;font-size:18px;font-style:italic;margin-top:0}.post-preview>.post-meta>a{text-decoration:none;color:#404040}.post-preview>.post-meta>a:hover,.post-preview>.post-meta>a:focus{color:#d66c79;text-decoration:underline}@media only screen and (min-width:768px){.post-preview>a>.post-title{font-size:36px}}.section-heading{font-size:36px;margin-top:60px;font-weight:700}.caption{text-align:center;font-size:14px;padding:10px;font-style:italic;margin:0;display:block;border-bottom-right-radius:5px;border-bottom-left-radius:5px}footer{padding:50px 0 65px}footer .list-inline{margin:0;padding:0}footer .copyright{font-size:14px;text-align:center;margin-bottom:0}.floating-label-form-group{font-size:14px;position:relative;margin-bottom:0;padding-bottom:0.5em;border-bottom:1px solid #eeeeee}.floating-label-form-group input,.floating-label-form-group textarea{z-index:1;position:relative;padding-right:0;padding-left:0;border:none;border-radius:0;font-size:1.5em;background:none;box-shadow:none!important;resize:none}.floating-label-form-group label{display:block;z-index:0;position:relative;top:2em;margin:0;font-size:0.85em;line-height:1.764705882em;vertical-align:middle;vertical-align:baseline;opacity:0;-webkit-transition:top 0.3s ease,opacity 0.3s ease;-moz-transition:top 0.3s ease,opacity 0.3s ease;-ms-transition:top 0.3s ease,opacity 0.3s ease;transition:top 0.3s ease,opacity 0.3s ease}.floating-label-form-group::not(:first-child){padding-left:14px;border-left:1px solid #eeeeee}.floating-label-form-group-with-value label{top:0;opacity:1}.floating-label-form-group-with-focus label{color:#0085a1}form .row:first-child .floating-label-form-group{border-top:1px solid #eeeeee}.btn{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;text-transform:uppercase;font-size:14px;font-weight:800;letter-spacing:1px;border-radius:0;padding:15px 25px}.btn-lg{font-size:16px;padding:25px 35px}.btn-default:hover,.btn-default:focus{background-color:#0085a1;border:1px solid #0085a1;color:white}.pager{margin:20px 0 0}.pager li>a,.pager li>span{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;text-transform:uppercase;font-size:14px;font-weight:800;letter-spacing:1px;padding:15px 25px;background-color:white;border-radius:0}.pager li>a:hover,.pager li>a:focus{color:white;background-color:#0085a1;border:1px solid #0085a1}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#808080;background-color:#404040;cursor:not-allowed}::-moz-selection{color:white;text-shadow:none;background:#0085a1}::selection{color:white;text-shadow:none;background:#d66c79}img::selection{color:white;background:transparent}img::-moz-selection{color:white;background:transparent}body{webkit-tap-highlight-color:#0085a1}code{margin-left:0.25em;margin-right:0.25em}pre code{margin:0}.ascii:before,.ascii:after{content:" "} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 00000000..b194a0c1 --- /dev/null +++ b/css/style.css @@ -0,0 +1 @@ +.notice{font-size:70%;margin-top:0.5em}span.author{margin-right:1em}span.meta{margin:1em}.hash-target{margin-bottom:1em}.hash-target:target{background-color:#FFFCA7;border-color:#FDE14F;border-width:1px;border-style:solid;border-radius:1px}#md-post-content table tr{border-top:1px solid #cccccc}#md-post-content table th,#md-post-content table td{border:1px solid #cccccc;padding:6px 13px}#md-post-content img{max-width:100%;height:auto}ul.social-buttons{list-style:none;vertical-align:middle;padding:5px}ul.social-buttons li{display:inline-block;vertical-align:middle}.navbar{background-color:#F3C677;border-color:#080808;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px}.navbar-brand>img{width:150px;position:relative;bottom:30%}.navbar .navbar-nav>li>a{color:#777;font-weight:bold;text-decoration:none}.navbar .navbar-nav>li>a:focus,.navbar .navbar-nav>li>a:hover{color:#000}#post-navigation{display:flex;align-items:center}#post-navigation div{display:flex;align-items:center}#post-navigation a{color:#313537;font-size:smaller}:target{margin-top:-60px;padding-top:60px}#table-of-contents-outer{margin-bottom:4ex}#table-of-contents{display:inline-block;border:1px solid #ccc;border-radius:4px;background-color:#f5f5f5;margin-left:5%;margin-right:10%;padding:0.5em 1em;font-size:80%}#table-of-contents .table-of-contents-title{font:normal x-large serif;margin-bottom:1ex}#table-of-contents a{font-family:sans;text-decoration:none}#table-of-contents a:hover{text-decoration:underline}#table-of-contents ul{padding-left:1em}#table-of-contents code{color:black;background-color:transparent}span.link-to-here-outer{display:inline-block;font:bold 8pt sans;vertical-align:middle;width:50pt;margin-left:-50pt}.link-to-here-outer a{text-decoration:none;color:white}span.link-to-here{display:inline-block;visibility:hidden;padding:5pt;background-color:#606060;vertical-align:middle}span.link-to-here-outer:hover a .link-to-here{visibility:visible}div.sourceCode{background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre.sourceCode{background-color:unset;border:unset;border-radius:unset}.jumbotron{color:#333}.btn{border-radius:4px} \ No newline at end of file diff --git a/css/syntax.css b/css/syntax.css new file mode 100644 index 00000000..3d654a9e --- /dev/null +++ b/css/syntax.css @@ -0,0 +1 @@ +pre>code.sourceCode{white-space:pre;position:relative}pre>code.sourceCode>span{display:inline-block;line-height:1.25}pre>code.sourceCode>span:empty{height:1.2em}.sourceCode{overflow:visible}code.sourceCode>span{color:inherit;text-decoration:inherit}div.sourceCode{margin:1em 0}pre.sourceCode{margin:0}@media screen{div.sourceCode{overflow:auto}}@media print{pre>code.sourceCode{white-space:pre-wrap}pre>code.sourceCode>span{text-indent:-5em;padding-left:5em}}pre.numberSource code{counter-reset:source-line 0}pre.numberSource code>span{position:relative;left:-4em;counter-increment:source-line}pre.numberSource code>span>a:first-child::before{content:counter(source-line);position:relative;left:-1em;text-align:right;vertical-align:baseline;border:none;display:inline-block;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;padding:0 4px;width:4em;color:#aaaaaa}pre.numberSource{margin-left:3em;border-left:1px solid #aaaaaa;padding-left:4px}div.sourceCode{}@media screen{pre>code.sourceCode>span>a:first-child::before{text-decoration:underline}}code span.al{color:#ff0000;font-weight:bold}code span.an{color:#60a0b0;font-weight:bold;font-style:italic}code span.at{color:#7d9029}code span.bn{color:#40a070}code span.bu{color:#008000}code span.cf{color:#007020;font-weight:bold}code span.ch{color:#4070a0}code span.cn{color:#880000}code span.co{color:#60a0b0;font-style:italic}code span.cv{color:#60a0b0;font-weight:bold;font-style:italic}code span.do{color:#ba2121;font-style:italic}code span.dt{color:#902000}code span.dv{color:#40a070}code span.er{color:#ff0000;font-weight:bold}code span.ex{}code span.fl{color:#40a070}code span.fu{color:#06287e}code span.im{color:#008000;font-weight:bold}code span.in{color:#60a0b0;font-weight:bold;font-style:italic}code span.kw{color:#007020;font-weight:bold}code span.op{color:#666666}code span.ot{color:#007020}code span.pp{color:#bc7a00}code span.sc{color:#4070a0}code span.ss{color:#bb6688}code span.st{color:#4070a0}code span.va{color:#19177c}code span.vs{color:#4070a0}code span.wa{color:#60a0b0;font-weight:bold;font-style:italic} \ No newline at end of file diff --git a/drafts/template.html b/drafts/template.html new file mode 100644 index 00000000..89f8c369 --- /dev/null +++ b/drafts/template.html @@ -0,0 +1,230 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +Haskell-jpなので基本的に日本語の記事を公開しましょう。
+Pandocのマークダウンを使えるので、脚注なども1このようにいれられます。
+ただの脚注です。 +テスト以外の何物でもありません。↩︎
誠に残念ながら、一般社団法人としての日本Haskellユーザーグループ管理委員会(通称Haskell-jp Admins)は、去る2022年4月16日を以て解散しました。先日、解散後に必要な処理のほぼすべてが完了しましたので、遅くなってしまいましたが、改めてこちらで報告致します。
+ +解散の背景を一言で申しますと、「お金がかかりすぎる上に、士気も下がってしまったから」です。設立前に調査した際、法人住民税のルールを誤解してしまったことで想定より費用がかさんだことや、当初活動の一環として掲げていた大規模なオフラインイベントが、現状のコロナ禍において難しく、その上、代表を含む社員自身の開催するモチベーションが下がってしまっていることを鑑みて、先般行われた第1期の社員総会で解散する事を合意しました。
+「一般社団法人」という法人格を失ってしまったので、法律上必要な場面において「日本Haskellユーザーグループ管理委員会」という名前を使用することはできなくなってしまいました。それでもできる範囲内で、任意団体として今までどおりの活動を継続したいと思います。
+具体的には、
+この記事はHaskell Advent Calendar 2021の25日目の記事です。
+Haskellのよく言われる問題点の一つとして、文字列型が下記のようによく使われるものだけで5種類もある、という点があります:
+String
Text
Text
ByteString
ByteString
(上記の頻繁に使われるもの以外にも、もっとあります)
+それぞれ確かに使いどころが違うので、アプリケーションで使用する場合は場面に応じて使い分ければいいのですが、文字列を使ったライブラリーを開発する場合はなかなか悩ましいものがあります。内部で依存しているライブラリーが使用しているものがあれば、それをそのまま使うのが簡単で確実ですが、そうでない場合も多いでしょう。そこで本稿では文字列型を抽象化して扱いたい場合の手段として、mono-traversalbeパッケージを検討したいと思います。
+mono-traversableパッケージは、名前のとおりMonoTraversable
やMonoFoldable
、MonoFunctor
といったおなじみの型クラスの名前にMono
という接頭辞を付けた型クラスによって、多様なコンテナ型を抽象化してくれます。これらの型クラスはすべて、ByteString
やText
のような、「要素として持てる値の型が1種類だけ」の型も対象にしているのが特徴です。Type Familyを応用し、次のように型毎に要素の型を固定することで、そうした特徴を実現しています:
type family Element mono
+
+type instance Element ByteString = Word8
+type instance Element Text = Char
+type instance Element [a] = a
+
+-- ...
+
+class MonoFunctor mono where
+-- ...
+ omap :: (Element mono -> Element mono) -> mono -> mono
+
+instance MonoFunctor ByteString where
+= ByteString.map
+ omap
+instance MonoFunctor Text where
+= Text.map
+ omap
+instance MonoFunctor [a] where
+= map omap
※mono-traversableパッケージのソースから引用して少し改変しました。
+さらに、これまで紹介したMonoTraversable
やMonoFoldable
、MonoFunctor
に加えて、SemiSequence
やIsSequence
という型クラスで分解や構築に関わる操作(例えばcons
やbreak
)などの他、(今回は取り上げませんが)SetContainer
などの型クラスでMap
やSet
、IntMap
などの型まで抽象化してくれます。
そこで次の節では、このmono-traversableパッケージにおける型クラスを中心に、Data.Text
モジュールやData.ByteString
モジュールにおける各関数が、どの型クラスに対するどの関数に対応するのか、まとめた表を作ってみました。
Monoid
、Semigroup
などのメソッドも調査対象に含めましたString
についてはbaseパッケージにある関数のみを対象にしていますが、Data.List
モジュールのドキュメントと自分の記憶を頼りに埋めているので間違いがあるかも知れませんText
・ByteString
についてはStrictなバージョンのドキュメントのみ参照しています。Lazyな方になかったらごめんなさい!Textual
型クラスについては、ByteString
がインスタンスになっていないのでご注意くださいIO
が絡むものText |
+ByteString |
+String ([Char] ) |
+型クラス / 関数 | +
---|---|---|---|
all |
+all |
+all |
+MonoFoldable / oall |
+
any |
+any |
+any |
+MonoFoldable / oany |
+
append |
+append |
+++ |
+Semigroup / <> |
+
N/A | +breakByte |
+N/A | +N/A | +
N/A | +breakEnd |
+N/A | +N/A | +
breakOnAll |
+N/A | +N/A | +N/A | +
breakOnEnd |
+N/A | +N/A | +N/A | +
breakOn |
+breakSubstring |
+N/A | +N/A | +
break |
+break |
+break |
+IsSequence / break |
+
center |
+N/A | +N/A | +N/A | +
chunksOf |
+N/A | +N/A | +N/A | +
commonPrefixes |
+N/A | +N/A | +N/A | +
compareLength |
+N/A | +N/A | +N/A | +
concatMap |
+N/A | +concatMap |
+MonoFoldable / ofoldMap |
+
concat |
+concat |
+concat |
+MonoFoldable / ofold |
+
cons |
+cons |
+N/A | +SemiSequence / cons |
+
copy |
+copy |
+N/A | +N/A | +
count |
+count |
+N/A | +N/A | +
dropAround |
+N/A | +N/A | +N/A | +
dropEnd |
+N/A | +N/A | +IsSequence / dropEnd |
+
dropWhileEnd |
+dropWhileEnd |
+dropWhileEnd |
+N/A | +
dropWhile |
+dropWhile |
+dropWhile |
+IsSequence / dropWhile |
+
drop |
+drop |
+drop |
+IsSequence / drop |
+
N/A | +elemIndexEnd |
+N/A | +N/A | +
N/A | +elemIndex |
+elemIndex |
+N/A | +
N/A | +elemIndices |
+elemIndices |
+N/A | +
N/A | +elem |
+elem |
+MonoFoldable / oelem |
+
empty |
+empty |
+"" |
+Monoid / mempty |
+
filter |
+filter |
+filter |
+IsSequence / filter |
+
N/A | +findIndexEnd |
+N/A | +N/A | +
findIndex |
+findIndex |
+findIndex |
+N/A | +
N/A | +findIndices |
+findIndices |
+N/A | +
N/A | +findSubstring |
+N/A | +N/A | +
N/A | +findSubstrings |
+N/A | +N/A | +
find |
+find |
+find |
+SemiSequence / find |
+
foldl' |
+foldl' |
+foldl' |
+MonoFoldable / ofoldl' |
+
foldl1' |
+foldl1' |
+foldl1' |
+MonoFoldable / ofoldl1Ex' |
+
foldl1 |
+foldl1 |
+foldl1 |
+N/A | +
foldl |
+foldl |
+foldl |
+N/A | +
N/A | +foldr' |
+N/A | +N/A | +
N/A | +foldr1' |
+N/A | +N/A | +
foldr1 |
+foldr1 |
+foldr1 |
+MonoFoldable / ofoldr1Ex |
+
foldr |
+foldr |
+foldr |
+MonoFoldable / ofoldr |
+
groupBy |
+groupBy |
+groupBy |
+IsSequence / groupBy |
+
group |
+group |
+group |
+IsSequence / group |
+
head |
+head |
+head |
+MonoFoldable / headEx |
+
index |
+index |
+index |
+IsSequence / indexEx |
+
init |
+init |
+init |
+IsSequence / initEx |
+
inits |
+inits |
+inits |
+N/A | +
intercalate |
+intercalate |
+intercalate |
+MonoFoldable / ointercalate |
+
intersperse |
+intersperse |
+intersperse |
+SemiSequence / intersperse |
+
isInfixOf |
+isInfixOf |
+isInfixOf |
+IsSequence / isInfixOf |
+
isPrefixOf |
+isPrefixOf |
+isPrefixOf |
+IsSequence / isPrefixOf |
+
isSuffixOf |
+isSuffixOf |
+isSuffixOf |
+IsSequence / isSuffixOf |
+
justifyLeft |
+N/A | +N/A | +N/A | +
justifyRight |
+N/A | +N/A | +N/A | +
last |
+last |
+last |
+MonoFoldable / lastEx |
+
length |
+length |
+length |
+MonoFoldable / olength |
+
lines |
+N/A | +lines |
+Textual / lines |
+
mapAccumL |
+mapAccumL |
+mapAccumL |
+N/A | +
mapAccumR |
+mapAccumR |
+mapAccumR |
+N/A | +
map |
+map |
+map |
+MonoFunctor / omap |
+
maximum |
+maximum |
+maximum |
+MonoFoldable / maximumEx |
+
minimum |
+minimum |
+minimum |
+MonoFoldable / minimumEx |
+
N/A | +notElem |
+notElem |
+MonoFoldable / onotElem |
+
null |
+null |
+null |
+MonoFoldable / onull |
+
pack |
+pack |
+id |
+IsString / fromString |
+
partition |
+partition |
+partition |
+IsSequence / partition |
+
replace |
+N/A | +N/A | +IsSequence / replaceSeq |
+
replicate |
+replicate |
+replicate |
+IsSequence / replicate |
+
reverse |
+reverse |
+reverse |
+SemiSequence / reverse |
+
scanl1 |
+scanl1 |
+scanl1 |
+N/A | +
scanl |
+scanl |
+scanl |
+N/A | +
scanr1 |
+scanr1 |
+scanr1 |
+N/A | +
scanr |
+scanr |
+scanr |
+N/A | +
singleton |
+singleton |
+singleton |
+MonoPointed / opoint |
+
snoc |
+snoc |
+snoc |
+SemiSequence / snoc |
+
N/A | +sort |
+sort |
+SemiSequence / sort |
+
N/A | +spanEnd |
+N/A | +N/A | +
span |
+span |
+span |
+SemiSequence / span |
+
splitAt |
+splitAt |
+splitAt |
+IsSequence / splitAt |
+
splitOn |
+N/A | +splitOn |
+IsSequence / splitSeq |
+
N/A | +splitWith |
+N/A | +IsSequence / splitElem |
+
split |
+splitWith |
+N/A | +IsSequence / splitWhen |
+
stripEnd |
+N/A | +N/A | +N/A |
+
stripPrefix |
+stripPrefix |
+stripPrefix |
+IsSequence / stripPrefix |
+
stripStart |
+N/A | +N/A | +N/A | +
stripSuffix |
+stripSuffix |
+N/A | +IsSequence / stripSuffix |
+
strip |
+N/A | +N/A | +N/A | +
tail |
+tail |
+tail |
+IsSequence / tail |
+
tails |
+tails |
+tails |
+N/A | +
takeEnd |
+N/A | +N/A | +N/A | +
takeWhileEnd |
+takeWhileEnd |
+N/A | +N/A | +
takeWhile |
+takeWhile |
+takeWhile |
+IsSequence / takeWhile |
+
take |
+take |
+take |
+IsSequence / take |
+
toCaseFold |
+N/A | +N/A | +Textual / toCaseFold |
+
toLower |
+N/A | +N/A | +Textual / toLower |
+
toTitle |
+N/A | +N/A | +N/A | +
toUpper |
+N/A | +N/A | +Textual / toUpper |
+
transpose |
+transpose |
+N/A | +N/A | +
uncons |
+uncons |
+N/A | +IsSequence / uncons |
+
unfoldrN |
+unfoldrN |
+N/A | +N/A | +
unfoldr |
+unfoldr |
+unfoldr |
+N/A | +
unlines |
+N/A | +unlines |
+Textual / unlines |
+
unpack |
+unpack |
+id |
+MonoFoldable / otoList |
+
unsnoc |
+unsnoc |
+N/A |
+SemiSequence / unsnoc |
+
unwords |
+N/A | +unwords |
+Textual / unwords |
+
words |
+N/A | +words |
+Textual / words |
+
zipWith |
+zipWith |
+zipWith |
+MonoZip / ozipWith |
+
zip |
+zip |
+zip |
+MonoZip / ozip |
+
以上です。残念ながら万能とはいかないようで、いくつか「N/A」、すなわち対応するものがない関数もありますが、他の関数の組み合わせで実装できるものもあるでしょう。
+MonoTraversable
などに限らず、型クラスを使って関数を多相化したとき全般に言えることですが、コンパイル時にインスタンスの解決が行えなかった場合、直接対象の型の相当する関数を呼ぶより少し遅くなってしまう場合があります(参考)。
また、それに限らず、各型クラスのメソッドでない関数は、各型の相当する関数でオーバーライドできないため、効率の悪い処理になってしまう恐れがあります。例えば、ointercalate
関数の実装を見ると、Text
やByteString
などについてはRULES
プラグマで最適な実装を設定しているようですが、それ以外の型については一旦リストに変換してから結合する、という効率の悪そうな処理をしています。
String
から相互変換できる型を抽象化する最後に、最近私が作った(まだリリースしてない)ライブラリーにおいて、MonoFoldable
とIsString
を使うことで、Text
とString
両方をサポートした関数を紹介しておきます。ただ、時間とやる気パワーが残り少なくなってしまったので、該当の箇所だけこちらからコピペして、説明は簡単にしておきます:
stringVal :: (IsString a, MT.MonoFoldable a, MT.Element a ~ Char) => CodecEnvVal a
+= valByFunction CodecEnvValByFunction
+ stringVal = MT.otoList
+ { encode = Right . fromString
+ , decode }
CodecEnvVal a
型は、a
型をString
型と相互変換するための情報を含んだ型です。stringVal
の場合、名前のとおり文字列っぽい型とString
との相互変換ができなければなりません。もちろん単純にString
型だけをサポートしてText
用には別途CodecEnvVal Text
を作ってもいいのですが、一つのCodecEnvVal a
だけで扱えた方が楽でしょうし、今回はMonoFoldable
のotoList
とIsString
のfromString
を使って両方をサポートすることにしました。なお、これではByteString
がサポートできませんが、ここで相互変換するString
は、要件上人間が読み書きするファイルにおける文字列を想定しているので、ByteString
はバイナリーデータにだけ使うべきだ、という立場から敢えてサポートしていません。
mono-traversableパッケージをうまく使えば、自前で専用の型クラスを作らなくてもString
・Text
・ByteString
などを一挙にサポートする関数が書けるかも知れません!
それでは2022年はmono-traversableでHappy Haskell String Programming!🚝
+この記事はググって解決しづらかったこと Advent Calendar 2021の25日目の記事です。Haskell-jp WikiのHaskellの歩き方というページにもほぼ同じことを書きましたが、今回はよい機会なので実例を加えつつ詳しく紹介させてください。
+よく知られているとおり、Haskellには二項演算子をプログラマーがかなり自由に定義できるという、とても変わった特徴があります。他のプログラミング言語でも使う標準的なもの(例: +
, *
, &&
など)を名前空間を絞って置き換えるほか、例えばかのlensパッケージのように、ライブラリーの作者があたかも新しい構文を作り上げるかのごとく独自の二項演算子を提供することができます。
これは面白い機能ではあるものの、しばしば混乱を招く機能でもあります。後述するその他の記号との区別がつきにくいですし、一般的な検索エンジンで検索することさえままなりません。Googleはプログラミングでよく使われる記号による検索をサポートはしているものの、Haskellでしか見ないような記号の組み合わせは到底無理でしょう。
+そんな背景もあり、Haskellを使う人はしばしばHoogleなどの、関数名で検索できる検索エンジンを使用することになります。こちらは二項演算子の名前での検索もサポートしています。
+例えばlensパッケージでおなじみの^.
で検索すると次のような結果になりました:
lensパッケージ以外でも、同様の^.
が定義されているのが分かりますね。lensパッケージは依存関係がとても大きい一方、^.
などの定義は十分単純でコピペしてもいいくらい小さいので、このようにいくつものパッケージで定義されています。
また、特によく使われる二項演算子はFPCompleteのウェブサイトでもまとめられています:
+ +Haskell、というかそのデファクトスタンダードな処理系であるGHCでは、言語拡張という形で長年新しい構文が提案されています1。その中には、当然これまでにない方法で記号を使っているものもあります。そうした記号はプログラマーが定義した関数ではないので、前述のHoogleなどを使った方法が通用しません。そこで、当ブログにも何度も寄稿いただいた@takenobu_hsさんが、言語拡張によるものも含めた、Haskellの構文における記号の一覧を作ってくださいました!
+takenobu-hs/haskell-symbol-search-cheatsheet
+実は日本語版もQiitaにあるのですが、上記のGitHub版の方が更新されているようです。そこで、今回はおまけとして、GitHub版の方にも載っている、GHCに最近(バージョン9.2.1以降に)追加された、新しいピリオド .
の使い方を紹介しましょう。
従来、Haskellでピリオドといえば関数合成を表す二項演算子でした:
+> f x = x + 1
+ ghci> g x = x * 3
+ ghci> h = g . f
+ ghci> h 2
+ ghci9 -- 2 に + 1 して * 3 した結果
数学における関数合成の記号「g ∘ f」に似せてピリオドを採用したのでしょう。しかし、世は今まさに大「ピリオドといえばフィールド2へのアクセス演算子じゃろがい」時代です。それでなくてもHaskellのレコード型は扱いにくいと言われているのに、フィールドへのアクセスまで変なやり方でした3:
+data SomeRecord =
+SomeRecord { field1 :: String, field2 :: Int }
+
+= SomeRecord "value1" 2
+ someRecord
+> field1 someRecord
+ ghci"value1"
+
+> field2 someRecord
+ ghci2
そこで、GHC 9.2からはOverloadedRecordDot
という言語拡張が導入され、これを有効にしたファイルではおなじみの言語のようにピリオドでレコードのフィールドにアクセスできるようになりました:
(以下はGHCiで使用した例です)
+> :set -XOverloadedRecordDot
+ ghci
+> someRecord.field1
+ ghci"value1"
+
+> someRecord.field2
+ ghci2
+
+-- ⚠️ピリオドの前後に空白を入れると関数合成として解釈されてしまう!
+> someRecord . field2
+ ghci
+<interactive>:5:1: error:
+? Couldn't match expected type ‘Int -> c’
+ type ‘SomeRecord’
+ with actual ? In the first argument of ‘(.)’, namely ‘someRecord’
+ In the expression: someRecord . field2
+ In an equation for ‘it’: it = someRecord . field2
+ ? Relevant bindings include
+ it :: SomeRecord -> c (bound at <interactive>:5:1)
OverloadedRecordDot
についてのより詳しい解説は、Haskell Day 2021における、fumievalさんの発表をご覧ください。
OverloadedRecordDot
拡張について勉強しましょう。🎁それでは2022年もHappy Haskell Hacking!!🎅
+余談: ghc-proposalsに送られたPull requestを見ると、今どのような提案が議論されているか分かります。↩︎
他のプログラミング言語では「プロパティー」と呼ばれることも多いですが、ここではHaskellのレコード型における用語に合わせました。↩︎
個人的にはゲッターが関数になるのはとても直感的な気がして割と好きでしたが、確かにデメリットもとても多い仕様でした。セッターは単純な関数になってないですしね。↩︎
Haskell Day 2021を開催します!
+ +こんにちはkakkun61こと岡本和樹です。
+この記事ではHaskell Day 2021の紹介と開催の経緯などを記載します。
+ +Haskell Dayは日本語で開催されるHaskellに関するイベントとしては最多の参加者を誇るイベントです。これまで2012・2016・2018・2019と開催してきました。新型コロナウイルスの影響により、残念ながら2020は開催しませんでしたが、2021はオンラインイベントとして開催します。
+このようなオンラインイベントの開催は未経験だったため、さまざまなイベント形式を検討した結果、今回は事前録画動画の予約公開という形式を採用しました。生放送ももちろん検討しましたが、ノウハウ不足の中で一発勝負という生放送はリスクが大きいという判断をしました。録画公開における臨場感の不足をおぎなうことを期待し、YouTubeのプレミア公開を使用しリアルタイムチャットによる発表者と視聴者・視聴者同士の交流をできるように予定しています。
+現在発表者募集中です!
+今回はオンライン開催ということで、お手数ですが発表者にもオンサイトのイベントと異なった準備をお願いすることになります。運営としてできるかぎりのサポートをしますので安心して応募いただければと思います。
+Connpassにて参加登録の受け付けもしていますので視聴者の方も登録をお願いします。
+その他のくわしい情報はHaskell Day 2021イベントページをご覧ください。
+みなさまのご応募をお待ちしています。またお体にお気をつけください。
+先日のHaskell-jp Adminsと同様に事務的な連絡で恐縮ですが、当ブログやHaskell-jpのSlack Workspace、GitHubのOrganizationなどにおけるコミュニケーションに適用される、「相互を尊重したコミュニケーションのためのガイドライン」を制定致しました。
+Haskell-jp 相互を尊重したコミュニケーションのためのガイドライン
+こちらはHaskell FoundationにおけるGuidelines for Respectful Communication (GRC)を日本語に翻訳し、運用主体などをHaskell-jpにおける実態に合わせて書き換えたものです。いわゆる「行動規範(Code Of Conduct。しばしば「COC」と略されます)」と同じ役割を果たすものですが、行動規範と異なり、禁止事項よりも推奨事項を数多く挙げているのが特徴です。このGRCを翻訳する前に、COCを提案した際の議論においてGRCのこうした特徴が好まれ、採用に至りました。
+このGRCは、今後Haskell-jpのSlack WorkspaceやHaskell-jpのGitHubにおけるOrganizationが管理するリポジトリー、それからHaskell-jpとして開催するイベントなど、様々な場面で適用されます。参加されるみなさんはご理解の上、快適なコミュニティー活動をお楽しみください。
+加えて、もちろん今秋開催予定のHaskell Day 2021においても、こちらのGRCを採用します。参加者、発表者、運営者の方々はご理解とご協力をよろしくお願いします。
+⚠️一般社団法人としての日本Haskellユーザーグループ管理委員会は、2022年4月16日を以て解散しました。今後は任意団体として活動を続けます。詳細は一般社団法人日本Haskellユーザーグループ管理委員会 解散のお知らせをご覧ください。
+以下では、記録のために設立当時の記事をほぼそのまま残しています。
+去る2021年2月9日、任意団体であり明確な会員資格を持たない、日本Haskellユーザーグループ(Haskell-jp)における共有財産やコミュニケーションの場の管理・運営を担う法人として、一般社団法人日本Haskellユーザーグループ管理委員会(通称 Haskell-jp Admins。法人番号 5020005014971)を設立しました。法人格を持つことを活かして、Haskell-jp Adminsは次の事業に取り組みます。
+そもそもの設立の動機は、山下さん(@nobsun)の好意によって個人名義で保有していたhaskell.jp ドメインを共同で管理出来るようにするためでした。ドメインを団体として保有するには、法人格と、法人名義の銀行口座が必要なのです。これ以外にも、Haskell-jpとして共有する価値のあるアカウントを管理する際の名義として、随時「日本Haskellユーザーグループ管理委員会」を使用します。
+Haskell-jp Adminsが出来たからといって、Haskell-jpのあり方が大きく変わることはありません。今後もSlackで質問したり議論したりブログ記事を書いたりしましょう!haskell.jpというドメインを活かし、「公式面して」自由に活動する方をいつでも待っています!
+我々Haskell-jp Adminsは、そうした活動をバックアップするために種々の問題に取り組んでいきます。
+2022/09/18 編集: 法人格を廃止するとともに、契約したバーチャルオフィスの規約に従い、こちらに記載していた住所も削除しました。
+こちらに一部個人情報を削除した上で掲載しています。
+ +🎅この記事は、Haskell Advent Calendar 2020 25日目の記事です。
+🎄Happy Christmas!!🎄
今回は先日(といっても元の質問の投稿からもう何ヶ月も経ってしまいましたが…)StackOverflowに上がったこちら👇の質問に対する回答の、続きっぽい話を書こうと思います。長いし、質問の回答からスコープが大きく外れてしまうので記事にしました。
+haskell - モナド則を崩してしまう例が知りたい - スタック・オーバーフロー
+Monad
とMonoid
にある重要な繋がりを説明した後、それを応用したWriter
Monad
がどうMonoid
を使ってMonad
則を満たしているのか証明します。そして、Writer
のそうした性質を用いて簡単にMonad
則を破る例を紹介することで、読者のみなさんがMonad
則のみならずdo
記法やMonad
そのものの性質について、よりはっきりとした理解が得られることを目指します。
本記事のサンプルコードは、Haskellの構文に準拠していないものを除いて、すべてreadme-testというツールの2020年12月13日時点の開発版でテストしました。こちらのツールはまだ開発中で、今後も仕様が大きく変わる可能性がありますが、この記事のサンプルコードをテストするのに必要な機能は十分にそろっています。このreadme-test自体についてはいつか改めて共有します。
+また、テストの際に用いた環境は以下の通りです:
+Monad
とMonoid
の切っても切り離せない関係「モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?」というフレーズ(原文「A monad is a monoid in the category of endofunctors, what’s the problem?」が示すとおり、モナドとモノイド、Haskellの識別子で言うところのMonad
とMonoid
には密接な関係があります。ぶっちゃけ、このフレーズの正確な意味を私は理解していないのですが、少なくともMonad
とMonoid
には重要な共通点があることは知っています。それは、どちらも単位元と結合則がある、ということです!
具体的にMonad
とMonoid
の単位元・結合則を見てみましょう:
Monoid
の単位元: 単位元であるmempty
は、どんな値x
に<>
で足しても結果が変わらない!
<> mempty = x
+ x mempty <> x = x
Monad
の単位元: return
は>>=
の前に使っても後ろに使っても、m
やk a
の結果を変えない!
return a >>= (\a -> k a) = k a
+>>= (\a -> return a) = m m
Monoid
の結合則: x <> y <> z
の結果は、y <> z
を先に計算しようとx <> y
を先に計算しようと変わらない!
<> (y <> z) = (x <> y) <> z x
Monad
の結合則: m >>= \x -> k x >>= h
の結果は、\x -> k x >>= h
を先に計算しようと、m >>= (\x -> k x)
を先に計算しようと変わらない!
>>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h m
※Monad
の単位元・結合則の式についてはわかりやすさのために引用元から少し形を変えています。
HaskellにおけるMonad
・Monoid
とは、値がそれぞれの単位元・結合則をを満たす型です1。それ以上でも、それ以下でもありません。
それぞれの単位元・結合則を表す式は、一見して異なるものに見えるかも知れませんが、表す性質自体はよく似ています。なので、式を読んでもよく分からないという方は、上記に書いた日本語の説明をざっと眺めて覚えておいてください。特に、結合則における「~を先に計算しようと、~を先に計算しようと変わらない!」の部分がこの後とても重要になります。
+Monoid
の例ここまで読んで、Monad
はなんか聞いたことがあるけどMonoid
は初めて聞くよ、という方向けに補足すると、Monoid
とは例えば次のような型の値(と、それに対する処理)です。
Sum
型: 数値(Num型クラスのインスタンス)に対する、足し算を表すMonoid
のインスタンス
-- これから紹介する処理に必要なモジュールのimport
+import Data.Monoid
-- Sum aに対する <> は + と同等なので、
+> getSum (Sum 1 <> Sum 2 <> mempty)
+-- は、
+1 + 2 + 0
+-- と同じ。
mempty
が各Monoid
のインスタンスにおける単位元を返す、という点に注意してください。上記のとおり足し算の場合は0
です。
Product
型: 数値(Num型クラスのインスタンス)に対する、かけ算を表すMonoid
のインスタンス
-- Product aに対する <> は * と同等なので、
+> getProduct (Product 1 <> Product 2 <> mempty)
+-- は、
+1 * 2 * 1
+-- と同じ。
リスト型: リスト型の値に対する、結合 (++)
を表すMonoid
のインスタンス
-- [a] に対する <> は ++ と同等なので、
+> [1, 2] <> [3] <> mempty
+-- は、
+1, 2] ++ [3] ++ []
+ [-- と同じ
All
型: Bool
型の値に対する論理積&&
を表すMonoid
のインスタンス
> getAll (All True <> All False)
+-- は、
+True && False
+-- と同じ
-- これが何を返すかは、想像してみてください!
+mempty getAll
Any
型: Bool
型の値に対する論理和||
を表すMonoid
のインスタンス
> getAny (Any True <> Any False)
+-- は、
+True || False
+-- と同じ
-- これも何を返すかは、想像してみてください!
+mempty getAny
このように、Monoid
は他のプログラミング言語でもおなじみの、多くの二項演算を表しています。これらのインスタンスはすべて、先ほど紹介した「単位元」や「結合則」のルールを守っているので、気になった方はぜひチェックしてみてください2。
Monoid
とWriter
の切っても切り離せない関係実はそんなMonad
とMonoid
の固い絆を象徴するようなMonad
が、この世にはあります。そう、Writer
です!Writer
はMonoid
の単位元・結合則をそのまま活かすことによってMonad
の単位元・結合則を満たしたMonad
であり、Writer
がどうやってMonad
則を満たしているのか知れば、Monad
則がどうやって成立するものなのかが、すっきりクリアになることでしょう。
手始めにWriter
の定義と、Writer
がMonad
の各メソッドをどのように実装しているか見てみましょう。「モナドのすべて」におけるWriter
の紹介ページから、少しリファクタリングしつつ引用します3。
-- Writer型の定義
+newtype Writer w a = Writer { runWriter :: (a, w) }
タプルに対してnewtype
していることから分かるとおり、Writer
の実態はただのタプルです。ただのタプルがどうやってMonad
になるのでしょう?その答えがこちら👇:
-- WriterのMonad型クラスの実装
+-- 実際のところFunctor, Applicativeのインスタンス定義も必要だけどここでは省略
+instance Monoid w => Monad (Writer w) where
+return a = Writer (a, mempty)
+ Writer (a, w1) >>= f =
+ let Writer (b, w2) = f a
+ in Writer (b, w1 <> w2)
return
の定義は比較的シンプルですね。mempty
を受け取った値a
と一緒にタプルに入れて返すだけです。Monad
の単位元であるreturn
では、Monoid
の単位元であるmempty
を使うのです。
一方、>>=
はどう読めばいいでしょう?let ... in ...
の結果にあたるWriter (b, w1 <> w2)
に注目してください。
まず、b
は>>=
の右辺であるf
が返した結果です。Writer
の>>=
が返す、Writer
がラップしたタプルの一つ目の要素は、ここでf
が返した値の型と一致していなければなりません。Writer
において>>=
の型はWriter w a -> (a -> Writer w b) -> Writer w b
であり、右辺にあたるf
は(a -> Writer w b)
という型なので、>>=
全体の戻り値Writer w b
とf
の戻り値が一致している必要があることがわかりますよね?
さらに重要なのがw1 <> w2
です。ここであのMonoid
の演算子<>
が出てきました!Writer
は>>=
の中で<>
を使うMonad
なんですね!一体何と何を<>
しているのでしょう?まず、<>
の左辺であるw1
は、左辺にあたるWriter
がタプルに保持していたMonoid
型クラスのインスタンスの値です。そして右辺のw2
は、>>=
の右辺に渡した関数f
がb
と一緒に返したw2
です。
以上のことをまとめると、Writer
の>>=
は、
(a, w1)
におけるa
をf
に渡して、f
が返した(b, w2)
におけるb
を、w1
とw2
と一緒に<>
でくっつけつつ返す、という処理を行っています。Writer
は、「b
を返すついでにw1
とw2
を<>
でくっつける」と覚えてください。
Writer
は、
Monad
の単位元return
でMonoid
の単位元mempty
を使って、Monad
の結合則を満たす>>=
で、これまたMonoid
の結合則を満たす<>
を使っているのです。やっぱりWriter
はMonoid
あってのMonad
と言えますね。
do
と<>
さて、この「b
を返すついでにw1
とw2
を<>
でくっつける」というWriter
の振る舞いが象徴するように、大抵のMonad
のインスタンスにおける>>=
は、何かしら値を返すついでに、何らかの処理を行うよう実装されています。この「ついでに行われる処理」はMonad
のインスタンスをdo
記法の中で扱うと、ますます静かに身を隠すようになります。
こちらもWriter
を例に説明しましょう。まず、例示用にWriter
を作るアクションを適当に定義します。
addLogging :: Int -> Int -> Writer [String] Int
+=
+ addLogging x y Writer (x + y, ["Adding " ++ show x ++ " to " ++ show y ++ "."])
+
+multLogging :: Int -> Int -> Writer [String] Int
+=
+ multLogging x y Writer (x * y, ["Multiplying " ++ show x ++ " with " ++ show y ++ "."])
addLogging
とmultLogging
はそれぞれ、引数として受け取った整数を足し算したりかけ算したりしつつ、「足したよ」「かけたよ」という内容の文字列を一緒に返します。Writer [String] Int
における[String]
にログとして書き込んでいるようなイメージで捉えてください。
これらをdo
の中で使ってみると、よりaddLogging
やmultLogging
が「足し算やかけ算をするついでに、ログとして書き込んでいる」っぽいイメージが伝わるでしょう:
testDo :: Writer [String] Int
+= do
+ testDo <- addLogging 3 4
+ result1 <- multLogging 5 2
+ result2 addLogging result1 result2
⚠️申し訳なくもdo
記法自体の解説、つまり>>=
がどのようにdo
記法に対応するかはここには書きません。お近くのHaskell入門書をご覧ください。
👆では、3 + 4
した結果result1
と、5 * 2
した結果result2
を足す処理を行っています。それに加えて、「足したよ」「かけたよ」というログを表す文字列のリスト[String]
も一緒に返しています。do
記法が>>=
に変換されるのに従い、Writer
の>>=
が内部で<>
を使い、addLogging 3 4
・multLogging 5 2
・addLogging result1 result2
が返した文字列のリスト[String]
を結合することによって、あたかもaddLogging
やmultLogging
が「値を返しつつ、ログとして書き込む」かのような処理を実現できるのがWriter
におけるdo
記法の特徴です。
能書きはここまでにして、実際にどのような結果になるか見てみましょう:
+> runWriter testDo
+17,["Adding 3 to 4.","Multiplying 5 with 2.","Adding 7 to 10."]) (
はい、3 + 4
と5 * 2
の結果を足し算した結果17
と、addLogging 3 4
・multLogging 5 2
・addLogging result1 result2
が一緒に返していた文字列のリスト[String]
が、書いた順番どおりに結合されて返ってきました。Writer
はdo
記法の中に書いたWriter
の値(a, w)
のうち、Monoid
のインスタンスであるw
を<>
で都度結合させているということが伝わったでしょうか?
Writer
Monad
の結合則とMonoid
の結合則ここまでで、Writer
Monad
がどのように<>
を使っているのか、それによって>>=
やdo
記法がどのように振る舞っているのか、具体例を示して説明いたしました。ここからは、Writer
がMonoid
の<>
の結合則をどう利用することで、Monad
としての>>=
の結合則を満たしているのかを示しましょう。長いので「めんどい!」という方はこちらをクリックしてスキップしてください。
そのために、Monad
の結合則における>>=
を、Writer
の>>=
として展開してみます。
(0) Monad
の結合則:
>>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h m
(1) m
は>>=
の左辺なのでWriter (a, w1)
に置き換える:
※ここからは、比較しやすくするために等式=
の左辺と右辺を別々の行に書きます。
let Writer (a, w1) = m in Writer (a, w1) >>= (\x -> k x >>= h)
+=
+ let Writer (a, w1) = m in Writer (a, w1) >>= (\x -> k x) >>= h
(2) 一つ目の>>=
をWriter
における>>=
の定義で置き換える:
let Writer (a, w1) = m
+Writer (b, w2) = (\x -> k x >>= h) a
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m in Writer (a, w1) >>= (\x -> k x) >>= h
(3) 等式=
の右辺における一つ目の>>=
も同様に変換する:
let Writer (a, w1) = m
+Writer (b, w2) = (\x -> k x >>= h) a
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m
+Writer (b, w2) = (\x -> k x) a
+ in Writer (b, w1 <> w2) >>= h
(4) 無名関数である(\x -> k x >>= h)
と(\x -> k x)
に、a
を適用する:
let Writer (a, w1) = m
+Writer (b, w2) = k a >>= h
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m
+Writer (b, w2) = k a
+ in Writer (b, w1 <> w2) >>= h
(5) 等式=
の左辺における二つ目の>>=
をWriter
における>>=
の定義で置き換える:
let Writer (a, w1) = m
+Writer (b, w2) =
+ let Writer (c, w3) = k a
+ Writer (d, w4) = h c
+ in Writer (d, w3 <> w4)
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m
+Writer (b, w2) = k a
+ in Writer (b, w1 <> w2)) >>= h
(6) 等式=
の右辺における二つ目の>>=
も同様に変換する:
let Writer (a, w1) = m
+Writer (b, w2) =
+ let Writer (c, w3) = k a
+ Writer (d, w4) = h c
+ in Writer (d, w3 <> w4)
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m
+Writer (b, w2) = k a
+ in let Writer (c, w3) = Writer (b, w1 <> w2)
+ Writer (d, w4) = h c
+ in Writer (d, w3 <> w4)
(7) Writer
は、Writer
と(a, w)
を切り替えるだけで実質何もしていないので削除する:
let (a, w1) = m
+=
+ (b, w2) let (c, w3) = k a
+ = h c
+ (d, w4) in (d, w3 <> w4)
+ in (b, w1 <> w2)
+ =
+ let (a, w1) = m
+= k a
+ (b, w2) in let (c, w3) = (b, w1 <> w2)
+ = h c
+ (d, w4) in (d, w3 <> w4)
(7.5) (7)の等式をよく見ると、=
の左辺においては(b, w2)
と(d, w3 <> w4)
が、=
の右辺においては(c, w3)
と(b, w1 <> w2)
が等しい。
let (a, w1) = m
+= -- ここの(b, w2)は、
+ (b, w2) let (c, w3) = k a
+ = h c
+ (d, w4) in (d, w3 <> w4) -- ここの(d, w3 <> w4)を代入したもの!
+ in (b, w1 <> w2)
+ =
+ let (a, w1) = m
+= k a
+ (b, w2) in let (c, w3) = (b, w1 <> w2) -- ここで代入している!
+ = h c
+ (d, w4) in (d, w3 <> w4)
(8) (7.5)から、=
の左辺ではb = d
でw2 = w3 <> w4
、=
の右辺ではc = d
でw3 = w1 <> w2
であることがわかる。なのでそれぞれ置き換える:
let (a, w1) = m
+= k a
+ (c, w3) = h c
+ (d, w4) in (d, w1 <> (w3 <> w4))
+ =
+ let (a, w1) = m
+= k a
+ (b, w2) = h b
+ (d, w4) in (d, (w1 <> w2) <> w4)
(9) a
~d
・w1
~w4
の変数名を、登場した順番に振り直す:
let (a, w1) = m
+= k a
+ (b, w2) = h b
+ (c, w3) in (c, w1 <> (w2 <> w3))
+ =
+ let (a, w1) = m
+= k a
+ (b, w2) = h b
+ (c, w3) in (c, (w1 <> w2) <> w3)
等式=
の左辺と右辺がそっくりな式になりましたね!
ここで、Monoid
の結合則を思い出してみましょう:
<> (y <> z) = (x <> y) <> z x
そう、x <> y <> z
などと書いて3つのMonoid
型クラスのインスタンスの値を<>
でくっつけるときは、カッコで囲って(y <> z)
を先に計算しようと、(x <> y)
を先に計算しようと、結果が変わらない、というものでした!
それを踏まえて、(9)の等式=
の両辺をよく見比べてみてください。異なっているのはw1 <> (w2 <> w3)
と(w1 <> w2) <> w3)
の箇所だけですね!つまり、Writer
Monad
における>>=
の結合則は、w1 <> (w2 <> w3)
と(w1 <> w2) <> w3)
が等しいから、すなわちMonoid
における<>
の結合則が成り立つからこそ成立するのです。これがまさしく「Monoid
とWriter
の切っても切り離せない関係」なのです!
それではいよいよ、「Monoid
とWriter
の切っても切り離せない関係」を利用して、Monad
則を破ってみましょう💣
<>
とMonoid
の結合則前述のとおり、Writer
における>>=
が結合則を満たすのは、Writer
がラップしているMonoid
な値の<>
が結合則を満たしてこそ、なのでした。これは言い換えれば、その、ラップしているMonoid
な値の<>
が結合則を破れば、自然にWriter
の>>=
も結合則を破るはずです。この方法は、結合則を満たさない>>=
っぽい処理をゼロから探すより遥かに簡単です。>>=
のようなm a -> (a -> m b) -> m b
というややこしい型の関数よりも、<>
のようなa -> a -> a
という型の関数の方がずっと身近ですしね!
Monoid
の<>
のようなa -> a -> a
という型の関数で、結合則を満たさない処理 — といえば、引き算-
や割り算/
を思い浮かべる方が多いのではないでしょうか。と、いうわけでMonoid
の例で紹介したSum
やProduct
のように、数値に対する引き算を表すnewtype
、Difference
を定義してみましょう:
newtype Difference a = Difference { getDifference :: a }
それから、Difference
を(実際には間違いですが)Monoid
のインスタンスにします。最近のGHCでは、Monoid
のインスタンスを定義する前にSemigroup
のインスタンスにする必要があるのでご注意ください。説明しやすさのために敢えてこれまで触れてきませんでしたが、これまで何度も使った<>
は実際のところMonoid
の関数ではなくSemigroup
の関数なんですね。Monoid
は「<>
で(結合則を備えた)二項演算ができるだけでなく、mempty
という単位元もある」という性質の型クラスなので、「単に『<>
で(結合則を備えた)二項演算ができる』だけの型クラスも欲しい!」というニーズから、Monoid
の<>
はSemigroup
の関数となり、Monoid
はSemigroup
のサブクラスという関係に変わったのでした。
何はともあれ、Difference
をSemigroup
のインスタンスにしましょう:
instance Num a => Semigroup (Difference a) where
+Difference a <> Difference b = Difference (a - b)
はい、単に両辺を-
で引き算するだけですね。
今度こそDifference
をMonoid
のインスタンスにします。本記事ではmempty
を直接使うことはないので何でもいいはずですが、とりあえずSum
と同様に0
ということにしておきます:
instance Num a => Monoid (Difference a) where
+mempty = Difference 0
😈これで<>
が結合則を満たさないおかしなMonoid
のインスタンス、Difference
ができました!早速試して結合則を破っていることを確認してみましょう:
-- こちらは 1 - (2 - 3) と同じ
+> getDifference $ Difference 1 <> (Difference 2 <> Difference 3)
+2
+
+-- こちらは (1 - 2) - 3 と同じなので...
+> getDifference $ (Difference 1 <> Difference 2) <> Difference 3
+-4 -- <- 当然 1 - (2 - 3) とは異なる結果に!
バッチリ破れてますね!このように<>
における結合則は、引き算などおなじみの演算で、簡単に破ることができます💪
>>=
とMonad
の結合則<>
における結合則を破ることができたと言うことは、Writer
の>>=
による結合則も、もはや破れたも同然です。先ほど定義したDifference
型を使えば、>>=
は途端に結合則を満たさなくなるでしょう。
例を示す前に、Writer
を使う際しばしば用いられる、ユーティリティー関数を定義しておきます。実践でWriter
を使いたくなったときにも大変便利なので、是非覚えておいてください:
tell :: Monoid w => w -> Writer w ()
+= Writer ((), w) tell w
このtell
関数は、受け取ったMonoid
な値をそのまま「ログとして書き込む」関数です。結果として返す値はただのユニット()
なので、気にする必要がありません。tell
のみを使ってWriter
を組み立てれば、「ログとして書き込む」値のみに集中することができます。これから紹介する例でもやはり関心があるのは「ログとして書き込む」値だけなので、ここでtell
を定義しました。
それではtell
を使って、Writer
の>>=
における結合則も破ってみましょう:
-- こちらは Difference 1 <> (Difference 2 <> Difference 3) と同じ
+> getDifference . snd . runWriter $ tell (Difference 1) >>= (\_ -> tell (Difference 2) >>= (\_ -> tell (Difference 3)))
+2
+
+-- こちらは (Difference 1 <> Difference 2) <> Difference 3 と同じなので...
+> getDifference . snd . runWriter $ (tell (Difference 1) >>= (\_ -> tell (Difference 2))) >>= (\_ -> tell (Difference 3))
+-4 -- <- 当然 1 - (2 - 3) とは異なる結果に!
予想どおり一つ目のWriter
と二つ目のWriter
とで異なる結果となりました。1 - (2 - 3)
と(1 - 2) - 3
をWriter
を使って遠回しに言い換えているだけなので、当然と言えば当然です。
しかしtell (Difference 1) >>= (\_ -> tell (Difference 2) >>= \_ -> tell (Difference 3))
などのWriter
型の式がMonad
の結合則m >>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h
にどう対応するのか、ちょっと分かりづらいですかね?(式も長いし)一つずつ注釈を加えます:
-- こちらは m >>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h の前半、
+-- m >>= (\x -> k x >>= h) に相当する
+> tell (Difference 1) >>= (\_ -> tell (Difference 2) >>= (\_ -> tell (Difference 3)))
+-- ^^^^^^^^^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-- m x k h
+--
+
+-- こちらは m >>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h の後半、
+-- (m >>= (\x -> k x)) >>= h に相当する
+> (tell (Difference 1) >>= (\_ -> tell (Difference 2))) >>= (\_ -> tell (Difference 3))
+-- ^^^^^^^^^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-- m x k h
ラムダ式の引数x
は実際には使われていない点に注意してください。これでもconst
を使って\x -> const (tell (Difference 2)) x
と書き換えれば、const (tell (Difference 2))
がk
に厳密に対応するので、上記の二組の式は>>=
の結合則を破るペアだと言えます。
do
記法とMonad
の結合則前の節では、Monoid
の結合則を守っていない値をラップしているWriter
を作ることで、>>=
の結合則を破る例を簡単に作り出せることを紹介しました。ここでは本記事の最後として、>>=
の結合則を破った結果、do
記法がいかに直感に反する挙動となるか紹介して、>>=
の結合則を守ることが私たちにどのようなメリットをもたらすのか解説します。
例として、先ほど>>=
の結合則を破るのに使った1 - 2 - 3
を再利用しましょう。Difference
をラップしたWriter
で1 - 2 - 3
を計算させると、次のような式になります:
Difference 1) >>= (\_ -> tell (Difference 2)) >>= (\_ -> tell (Difference 3)) tell (
これをdo
記法に変換すると、次のようになります:
do
+Difference 1)
+ tell (Difference 2)
+ tell (Difference 3) tell (
do
記法における各行の間に>>=
が隠れたことで、すっきりしましたね!
この状態から、do
記法を使って1 - (2 - 3)
と(1 - 2) - 3
を表すWriter
の式にするには、次のように書き換えます:
-- こちらが 1 - (2 - 3) を表す
+=
+ do_1minus'2minus3' do
+ Difference 1)
+ tell (do
+ Difference 2)
+ tell (Difference 3)
+ tell (
+-- こちらが (1 - 2) - 3 を表す
+=
+ do_'1minus2'minus3 do
+ do
+ Difference 1)
+ tell (Difference 2)
+ tell (Difference 3) tell (
コメントに書いたとおり、do_1minus'2minus3'
が1 - (2 - 3)
、do_'1minus2'minus3
が(1 - 2) - 3
と同等なWriter
です。Haskellはシングルクォートを変数の名前に含めることができるので、シングルクォートでカッコを表すことにしました(まさかこんなところで役に立つとはね!)。
上記の二つの式では、カッコ()
で囲う代わりにもう一つのdo
記法に収めることで、do
記法における各行を実行する順番をいじっています。
本当にこれで1 - (2 - 3)
や(1 - 2) - 3
と同等な式になっているのでしょうか?試しにrunWriter
して結果を確かめてみましょう:
-- こちらが 1 - (2 - 3) を表す
+> getDifference . snd $ runWriter do_1minus'2minus3'
+2
+
+-- こちらが (1 - 2) - 3 を表す
+> getDifference . snd $ runWriter do_'1minus2'minus3
+-4
バッチリ👌想定どおり、do_1minus'2minus3'
が1 - (2 - 3) = 2
を計算し、do_'1minus2'minus3
が(1 - 2) - 3 = -4
を計算していますね!
さてこれまでで、Writer
Monad
はMonoid
の結合則を利用することで>>=
の結合則を満たしていることを示し、ラップしているMonoid
な値が結合則を満たしていなければ、必然的にWriter
も結合則を破ってしまうことを、>>=
やdo
記法を使って具体的に示しました。それでは今挙げた、do
記法で結合則を破った例は、一体何を示唆しているのでしょうか?普通にHaskellでコードを書いていて、前述のような書き換え、すなわち、
do
+Difference 1)
+ tell (do
+ Difference 2)
+ tell (Difference 3) tell (
から、
+do
+do
+ Difference 1)
+ tell (Difference 2)
+ tell (Difference 3) tell (
への書き換え(あるいはその逆)は、一見するとそんな機会ないように思えます。しかしこれが、do
記法をカッコ代わりに使うという変な方法ではなく、次のように変数に代入することで切り出していた場合、いかがでしょうか?
= tell (Difference 1)
+ someSingleAction
+= do
+ someSequence Difference 2)
+ tell (Difference 3)
+ tell (
+= do
+ someCompositeAction
+ someSingleAction someSequence
上記👆のような三つのWriter
の値を、下記👇の三つの値にリファクタリングする場合です。
= do
+ refactoredSequence Difference 1)
+ tell (Difference 2)
+ tell (
+= tell (Difference 3)
+ splitOutSingleAction
+= do
+ refactoredCompositeAction
+ refactoredSequence splitOutSingleAction
あるいは、たった3行しかありませんし、一つの値に統合する方がいいかも知れません:
+= do
+ flattenedAction Difference 1)
+ tell (Difference 2)
+ tell (Difference 3) tell (
これらの書き換えは、いずれもdo
記法が内部で使っている>>=
の結合則を前提とすれば、可能であってしかるべきです。do
記法は、適当にMonad
のインスタンスの値(「アクション」などとも呼ばれます)を上から下まで列挙すれば、自動で>>=
を使ってつなげてくれる、というものです。なので、適当に並べたアクションがどういう形に結合されるのか気にする必要があるのでは、安心して使えません。一方、上記の3組の式は、Writer Difference
、すなわち引き算を表す「偽Monoid
」をラップしているが故に、>>=
の結合則を満たしておりません。結果、do
記法に変えたときに並べたアクションをどこで切り出すかで、結果が変わってしまいます。これでは安心して列挙できません!
以上です。これまでで、Monad
則のうち結合則がなぜ重要なのか、結合則を実際に破ってみることを通じて説明しました。Monad
と同様に結合則を持ったMonoid
は、Monad
以上にインスタンスを見つけるのが簡単で、なおかつ、例えば引き算のように「二項演算だけど結合則を満たしていない」処理を見つけるのが簡単です。本記事ではMonoid
のそうした性質と、Monoid
の性質でもってMonad
則を満たしているWriter
Monad
に注目することで、簡単にMonad
則を破る例を提示することができました。それから、Monad
の結合則を実際に破った例を使って、Monad
の結合則がdo
記法を自然に書けるようにするために必要であることを示しました。これらの実例から主張したいことを一般化すると、次のとおりです:
do
記法の各行の間で、値を返すついでに何かを行うのがMonad
のインスタンスdo
記法の各行の間で、値を返すついでに行っている処理が結合則を満たす型が、Monad
則を満たすと言えるMonad
則を守らない型をdo
記法で使うと、do
記法の結合を気にして書かなければならなくなるそれでは、2021年も🎁Happy Haskell Hacking with Monad🎁!
+一応、Monad
についてはそのスーパークラスであるApplicative
の則、Functor
の則がありますが、Monad
則を満たしていればそれらは自動的に満たせるので、ここでは省略します。↩︎
残念ながら実際のところ、Float
型・Double
型などの浮動小数点数に対するSum
やProduct
は結合則を満たさない場合があります。これは他の多くのプログラミング言語にもある、浮動小数点数の悩ましい問題です。詳しくは「情報落ち」で検索してみてください。↩︎
ここでの定義は、実際に使われているtransformersパッケージのWriter
の定義とは大きく異なっているのでご注意ください。実際のWriter
はパフォーマンス上の都合やMonad Transformerとの兼ね合いで、幾分工夫された定義となっています。↩︎
Haskell-jpのコンテンツの一つとしてHaskell AntennaというWebページの開発・運用をしております。
+バイナリのビルドやDockerイメージのビルドにTravisCIを、バイナリを実行してページの更新をするのにDroneCIを使っていました。 +しかし、長らく放置していてちゃんと動作しているか怪しかったので、メンテナンスをするついでに昨今はやり(要出典)のGitHub Actionsにこれらを移行することにしました。
+まずはバイナリのビルドを行うように設定します。
+Haskell AntennaのプログラムはHaskell Stackを利用しているので、stack build
が実行できれば良いです。
name: Build Application
+on:
+ pull_request: null
+ push:
+ branches:
+ - master
+jobs:
+ build:
+ name: ${{ matrix.os }}
+ runs-on: ubuntu-18.04
+ strategy:
+ fail-fast: false
+ matrix:
+ ghc: ["8.8.4"]
+ steps:
+ - uses: actions/checkout@v2
+ - name: Cache .stack
+ id: cache-stack
+ uses: actions/cache@v2
+ with:
+ path: ~/.stack
+ key: "\
+ ${{ runner.os }}-stack\
+ -${{ hashFiles('**/stack.yaml.lock') }}\
+ -${{ hashFiles('**/package.yaml') }}\
+ "
+ restore-keys: |
+
+ ${{ runner.os }}-stack- - uses: haskell/actions/setup@main
+ name: Setup Haskell
+ with:
+ ghc-version: ${{ matrix.ghc }}
+ enable-stack: true
+ stack-version: 'latest'
+ - name: Install dependencies
+ run: stack --system-ghc test --only-dependencies
+ - name: Build and Test
+ run: stack --system-ghc test --copy-bins --local-bin-path=./bin
これは、PRが作られたときやmasterがプッシュされたときに実行されることを想定しています。
+GitHub ActionsでHaskellやHaskell Stackを使うには、公式が提供しているactions/setup-haskell haskell/actions/setup を利用します。
+元々はactions/haskell-setupがありましたが、どうやらメンテナンスする人がいなくなったっぽくアーカイブされてしまいました。
+この記事を書いている時点では移行したばかりでちゃんとタグが切られていないため、mainブランチを指定しています。
+ちなみに、StackプロジェクトのGHCバージョンをhaskell/actions/setupでインストールして、stack --system-ghc
をすることでキャッシュサイズを減らすことができます。
これまた余談ですが、actions/setup-haskellの方を使っていて次のようなエラーが出る場合はactions/setup-haskellのバージョンが古いです(最新では修正済みです)。haskell/actions/setupの方を使いましょう。
+Installing ghc version 8.8.4
+Error: Unable to process command '::add-path::/opt/ghc/8.8.4/bin' successfully.
+ Error: The `add-path` command is disabled. Please upgrade to using Environment Files or opt into unsecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/
antennaプログラムはDockerイメージにしてDocker Hubに置いてあります(これもGitHub Container Registryに移行したいですね)。 +なので、masterの更新に合わせてDockerイメージをビルドしてプッシュするジョブを設定します。 +Dockerイメージのビルドとプッシュにはdocker/build-push-actionを使います。
+# さっきと同じ設定ファイルです
+name: Build Application
+on:
+ pull_request: null
+ push:
+ branches:
+ - master
+jobs:
+ build:
+ name: ${{ matrix.os }}
+ runs-on: ubuntu-18.04
+ strategy:
+ fail-fast: false
+ matrix:
+ ghc: ["8.8.4"]
+ steps:
+ ... # 割愛
+ - name: Build and Test
+ run: stack --system-ghc test --copy-bins --local-bin-path=./bin
+ # Build and Push Docker Image
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@master
+ with:
+ platforms: all
+ - name: Setup Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@master
+ with:
+ version: latest
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ builder: ${{ steps.buildx.outputs.name }}
+ tags: haskelljp/antenna:latest
+ push: ${{ github.event_name != 'pull_request' }}
+ build-args: local_bin_path=./bin
masterブランチへのプッシュのときにだけDockerイメージのプッシュをして欲しいので、push:
に github.event_name != 'pull_request'
を設定しています。
+また、Haskell Stackでビルドされたバイナリファイルは--local-bin-path=./bin
オプションで./bin
に置いてあります。
+これをDockerfileでコピーするようにしている(下記参照)ので、docker build
の引数にlocal_bin_path=./bin
というのを与える必要がありました。
FROM matsubara0507/ubuntu-for-haskell:git
+ARG local_bin_path
+RUN mkdir -p /root/.local/bin && mkdir -p /work
+ENV PATH /root/.local/bin:$PATH
+WORKDIR /work
+COPY ${local_bin_path} /root/.local/bin
このように前のstepまでの結果を利用するには context: .
を指定する必要があります(デフォルトではgit-contextというのを使うからです)。
最後に、masterの更新があったときにantennaプログラムを実行してHaskell Antennaページを更新するような設定をします。 +日毎のスケジュール実行も設定したいので、新しいワークフローを切りました。
+name: Update Antenna page
+on:
+ schedule:
+ - cron: '0 8 * * *'
+ push:
+ branches:
+ - master
+ paths-ignore:
+ - 'README.md'
+ - 'CHANGELOG.md'
+ - 'LICENSE'
+ - '.gitignore'
+jobs:
+ update:
+ name: ${{ matrix.os }}
+ runs-on: ubuntu-18.04
+ strategy:
+ fail-fast: false
+ matrix:
+ ghc: ["8.8.4"]
+ steps:
+ ... # Install dependenciesまでは一緒なので割愛
+ - name: Build
+ run: stack --system-ghc build
+
+ - uses: actions/checkout@v2
+ with:
+ ref: 'gh-pages'
+ path: 'temp'
+ - name: Exec Application
+ run: |
+
+ cp sites.yaml temp/sites.yaml
+ cp -r image/* temp/image
+ cd temp && stack exec -- antenna sites.yaml - name: Push changes
+ env:
+ COMMIT_MESSAGE: Update haskell antenna. See https://haskell.jp/antenna/ for new entries!
+ run: |
+
+ git config --local user.email "bot@example.com"
+ git config --local user.name "Bot"
+ git status
+ git add -A
+ git diff --staged --quiet || git commit -m "$COMMIT_MESSAGE"
+ git push origin gh-pages working-directory: ./temp
バイナリをビルドするところまでは一緒です。 +Haskell Antennaは同じリポジトリのgh-pagesブランチに置いて、GitHub Pagesを使って公開しています。 +なので、同じリポジトリのgh-pagesブランチをgit cloneしなおしてサブディレクトリに置き、そこでantennaプログラムを実行して、更新があった場合にのみプッシュしています。 +同じリポジトリであれば、特に設定することなくプッシュできるのがGitHub Actionsのメリットですね。
+ついでに最近のアップデートによって、ZennをHaskell Antennaに載せるサイトへ追加しました(igrep氏がしてくれました、ありがとうございます)。 +アイコンの利用規約などがわからなかったのですが、GitHubのPR上で直接聞いてみたところ、問題ないという回答をいただきました。 +突然だったのにありがとうございます。
+Haskell プログラミングにおいて,データ型は非常に重要な役割を持つ.データ型は,扱うデータをプログラミング上で安全かつ容易に加工するために用いられ,またデータに対してどのような操作ができるのかを規定する.
+Haskell には,データ型を新たに定義する方法が3つある.
+type
キーワードによって定義する方法で,これにより定義されたデータ型は型シノニムと呼ばれる.data
キーワードによって定義する方法で,これにより定義されたデータ型は代数的データ型と呼ばれる.newtype
キーワードによってある型を元に新たな型を作る方法だ.今回は,それぞれどういう使い方をするのか,どういう違いがあるのかについて見ていきたいと思う.
+例えば,あなたは Web サイトを運営していて,一部年齢制限が必要なため,人の年齢が 20 歳以上かを判定する関数を書かなければいけないとする.年齢は整数だが,入力は必須でないため入力してない人もいる.その場合は,20 歳以上でないと判定する.この関数は,
+isAdult :: Maybe Int -> Bool
+= case m of
+ isAdult m Nothing -> False
+ Just x -> x >= 20
と書ける.ただ,この定義はどこか味気ない.isAdult
が受け取るデータは,年齢を表していて,整数か未詳かの状態を持つので,Maybe Int
はデータを正確に捉えられている.しかし,Maybe Int
に適合するデータは他に無数にあるため,isAdult
が受け取るデータが年齢を表すのか知能指数を表すのか,はたまた今までお酒を飲んだことのある回数なのかは推測しないと分からない.年齢を表すデータ型を新たに定義して,それを受け取るようにすればもっとプログラムがクールになるだろう.
Haskell で新しくデータ型を定義する最も簡単な方法は,type
キーワードを使って型シノニム (type synonym) を定義する方法だ.シノニムとは,別名という意味で,型シノニムは文字通り,ある型の別名を表す.今回は次のように使える:
type Age = Maybe Int
+
+isAdult :: Age -> Bool
+= case age of
+ isAdult age Nothing -> False
+ Just x -> x >= 20
これで関数 isAdult
は,先ほどと比べてとても明確になった.Age
は Maybe Int
を元に作られた型シノニムで,つまり Age
は Maybe Int
の別名になっている.単なる別名なので,isAdult
は Maybe Int -> Bool
型の関数だと思って使うこともできる.GHCi で試してみよう:
>>> (isAdult :: Maybe Int -> Bool) (Just 22 :: Age)
+True
Maybe Int
を Age
だと思うこともできるしその逆もできる.型シノニムと元となった型は自在に取り替え可能だ.型シノニムはとても手軽なので,Haskell の標準ライブラリでも使われている.例えば,次のようなデータ型が型シノニムで定義されている:
type String = [Char]
+type FilePath = String
文字列は文字のリストと見做せる.そこから文字列によるデータ型 String
は,単に文字のリスト型の型シノニムで定義されている.文字列に対してリストの関数を自由に適用できるのは,このためだ.ファイルのパスによるデータ型 FilePath
は String
の型シノニムで定義されている.なので,文字列の関数を自由に適用できる.
Haskell の型シノニムは,これだけに止まらずもっと強力な機能も持っている.例えば,型シノニムは型コンストラクタ,すなわち型を受け取って新たな型を作るコンストラクタに対しても作れる:
+type Option = Maybe
この型シノニムを使うと,Maybe Int
と書く代わりに Option Int
と書くことも可能だ.部分適用された型コンストラクタに対する型シノニムも書ける:
type Failable = Either String
この型シノニムを使うと,Either String ()
と書く代わりに Failable ()
と書くことができる.
さらに型シノニムは,パラメータを持つことができる:
+type List a = [a]
この型シノニムを使うと,[Int]
は List Int
と書ける.ただし,型シノニムはあくまで別名なので,全てのパラメータを適用した状態でしか書けないことに注意する必要がある.例えば,次のプログラムはコンパイルエラーになる:
type Apply f a = f a
+type ApplyMaybe = Apply Maybe
Apply
は2つのパラメータをとるが,ApplyMaybe
は Apply
に1つのパラメータしか渡していない.この場合,Apply Maybe
という型がどういう型の別名になるか Haskell は分からないため,この型を拒否する.このプログラムを修正するには,
type Apply f a = f a
+type ApplyMaybe a = Apply Maybe a
というように,Apply
に全ての引数を渡してやる必要がある.こうすることで,Haskell は Apply
の定義から Apply Maybe a
が Maybe a
の別名であると認識できるようになる 1.
型シノニムは,他にも幾つか用途上で制限がある.1つ目は再帰的な型シノニムが作れないという制限だ.例えば,
+type InfiniteList a = (a, InfiniteList a)
という定義は Haskell では却下される.相互再帰的な定義も許容されていない:
+type Rec1 = [Rec2]
+type Rec2 = [Rec1]
Rec1
の型を具体的に求めようとすると,[Rec2]
の型になる.Rec2
はやっぱり型シノニムで,[Rec1]
の別名なので,この型はさらに [[Rec1]]
という型になる.このようにして具体的な型を求めようとしても永遠に型シノニムがどこかしらに入り込むことになってしまい,型シノニムが現れない型を求めることはできない.Haskell ではそのようなことがないように,そのような定義を排除している 2.
もう1つの制約は,型シノニムを型クラスのインスタンスとして使えないというものだ.例えば,次のようなことはできない:
+type I = Int
+
+class C a
+instance C I
代わりに,
+class C a
+instance C Int
というように型シノニムを使わず書く必要がある.これは型シノニムを使って書けない唯一の例外だ.ただ,この制限は本質的なものではなく,Haskell 標準で型シノニムに対する混乱を避けるための制限になっている.もし,型シノニムに対してインスタンスを書けるようにしても,型シノニムは単なる別名なので,それは元となった型に対してインスタンスを定義してることと同じになる.このため,
+f :: C a => a -> a
+= x f x
という関数は,type Age = Int
による型シノニム Age
に対して C
のインスタンスが定義されていた場合,a
が Age
の場合も Int
の場合も許容される.これは,プログラマが意図していない動作かもしれない.つまり,年齢のデータだけにインスタンスを定義したつもりが,整数データ全般に対していつのまにかインスタンスを定義してしまったことになるからだ 3.
これらの制限はあるものの,型シノニムはデータ型を定義する上でとても強力で,しかも簡単に使用できる機能だ.
+さて,型シノニムでデータ型を定義する場合には幾つかの制限があった.では,この制限を超えたデータ型を定義する方法はないのだろうか? そのような場合には代数的データ型 (algebraic datatype) を使うことができる.
+代数的データ型は,複数の型の値を統合して1つの型の値として扱うデータ型の積と,複数の型の表現範囲を合わせて1つの型として扱うデータ型の和を組み合わせることで構成されている.そして,このデータ型の定義は,型シノニムと異なり完全に新しい型を作り出す.実際の例を見てみよう.
+あなたは積木パズルのパーツそれぞれの面積を計算する関数を,書かなければいけない.積木パズルのパーツはそれぞれ,長方形,真円,三角形から構成されている.まずはこのパーツを Haskell のデータ型に落とし込む必要がある.それぞれのパーツにおいて,
+によって特徴付けられている.では,これを代数的データ型に落とし込んでみよう:
+data PuzzleElement
+= Rect
+ Double
+ -- ^ 縦の長さ
+ Double
+ -- ^ 横の長さ
+ | Circle
+ Double
+ -- ^ 半径
+ | Triangle
+ -- ^ 三つの辺の長さを与える
+ Double Double Double
この定義は,PuzzleElement
という新しい型を作り,3つの値コンストラクタを作る.それぞれ
Rect :: Double -> Double -> PuzzleElement
Circle :: Double -> PuzzleElement
Triangle :: Double -> Double -> Double -> PuzzleElement
という型を持つ.Rect
は Double
型の値を2つ受け取り,その2つの値を PuzzleElement
型の1つの値として統合する.つまり,Double
型2つの積を作る.Circle
や Triangle
も同様だ.そして,PuzzleElement
型は3種類の積の値のいずれかを表し,すなわちこれら3種類の積の和を表す.このように,積和によって新しいデータ型を定義できるのが data
宣言であり,それによって定義されるのが代数的データ型になる.
代数的データ型の値から統合した値を取り出したい時は,case
文を使ったパターンマッチを行う:
areaMeasure :: PuzzleElement -> Double
+= case x of
+ areaMeasure x Rect w h -> w * h
+ Circle r -> r * r * pi
+ Triangle s1 s2 s3 ->
+ let s = (s1 + s2 + s3) / 2
+ in sqrt $ s * (s - s1) * (s - s2) * (s - s3)
areaMeasure
によってパズルのピースの面積を求めることができるようになった.
前に紹介した型シノニムは,ある型に対してその別名を与えるだけだった.それに比べ,代数的データ型では新しいデータ型を作り,その型の値を作る値コンストラクタを定義する.そして,型シノニムと大きく異なる点は,型システム上からは新たに定義された型しか分からず,実際にそのデータ型がどういう型から構成されるか分からない点にある.PuzzleElement
型の値は,もしかしたら Double
型の2つの値から Rect
コンストラクタを介して作られているかもしれないし,Double
型1つの値から Circle
コンストラクタを通して作られているかもしれない.これは実行時にその関数でパターンマッチをしてみて初めて分かることだ.型シノニムでは,型システムからそれがどういう型を元にしていたか分かるが,代数的データ型で観測できるのは新たに作られたデータ型があることだけだ.この違いは,代数的データ型と型シノニムの制約の違いに表れてくる.代数的データ型では,型シノニムの時に挙げたような制約はない.
例えば,代数的データ型は型シノニムと同様,パラメータをとることができ,さらに部分適用も可能だ 4:
+data Apply f a = Apply (f a)
+type ApplyMaybe = Apply Maybe
これは Haskell の正しいプログラムになる.Apply
は,2つのパラメータをとる型コンストラクタになっていて,データ型 Apply f a
の値を作る方法として,f a
型の値から値コンストラクタ Apply :: f a -> Apply f a
を通す方法がある.ApplyMaybe
は Apply Maybe
の型シノニムになっていて,これを使えば Apply Maybe Int
と書く代わりに ApplyMaybe Int
と書けるようになる.ApplyMaybe
の定義は,Apply
に対して1つのパラメータしか渡していない.にも関わらず正しいというのが,型シノニムと異なる点になる.
再帰的なデータ型を代数的データ型で定義することも可能だ:
+data List a
+= Cons a (List a)
+ | Nil
データ型 List a
は a
型の要素を持つ単連結リストを表す.値コンストラクタが List a
型の値を受け取ることがポイントだ.型シノニムでは,その型の定義に自身を含めることはできなかった.これは実際の具体的な型を求めようとした時,その計算が永遠に終わらなくなってしまうからだった.代数的データ型 List a
ではその型は単に新しい型として作られ,実際にその型の値がどういう型の値によって構成されているか知る必要はない.List a
はそれ自体が具体的な型であり 5 ,それ以上計算する必要はないからだ.代数的データ型において,定義された型とその型の値を作る方法は分離されている.そのため,データ型の計算においてその型の値を作る方法は考慮されない.よって,自身が定義中で用いられても,型シノニムのようにデータ型の計算が永遠に終わることがないということはないため,その操作が許容されている.
もちろん,新しい型が定義されるため,型クラスのインスタンスを混乱なく定義できる.代数的データ型を作成した時,基本的なインスタンスを定義することは Haskell プログラミングにおいてよくあることだ.Haskell では,言語機能としてそれを支援する機能がある.それは,deriving
構文というもので,Eq
/ Ord
などの標準的な型クラスを,データ型の定義から自動で導出してくれる.例えば,List a
に対して使ってみると,以下のようになる:
data List a
+= Cons a (List a)
+ | Nil
+ deriving (Eq, Ord, Show)
このように代数的データ型は,型シノニムでは定義できなかったデータ型を定義することができる.そして,代数的データ型は全く新しい型を作ることもできる:
+data Nat
+= Succ Nat
+ | Zero
このデータ型 Nat
は,他の型には依存しない全く新しい型だ.このように,代数的データ型は型シノニムと異なり全く新しい構造を作り出すことができる.
ただ,その代わり既存の関数を流用できなくなってしまう場合がある.例えば,
+data Tuple a b = Tuple a b
は,(a, b)
と構造が同じであり,(a, b)
に対する関数 fst :: (a, b) -> a
を適用できてもいいはずだ.ところが,データ型 Tuple a b
とその値コンストラクタは型システム上は切り離されているため,自身の値が (a, b)
の値と同じ方法でしか構成できないことを知らない.Tuple a b
と (a, b)
において型上で言及できることは,それらが異なる型であるということだけだ.なので,fst
に Tuple a b
型の値を渡すことはできない.これは,もし型シノニムを使って,
type Tuple a b = (a, b)
と定義した場合は解決する問題だ 6.
+このように両者にはトレードオフがあり,利用目的に合った使い分けをするのがいいだろう.
+さて,data
宣言の構文は他に2つ,便利な機能がある.
1つは正格性フラグと呼ばれる機能で,値コンストラクタにおいて引数を正格に評価することを強制できる.例えば,
+data StrictTuple a b = StrictTuple !a !b
というように,正格性フラグ !
を使った定義を行うと,値コンストラクタ StrictTuple :: a -> b -> StrictTuple
はその引数を正格に評価してから格納するようになる.通常,
data Tuple a b = Tuple a b
のように正格性フラグを使わない定義では,
+>>> case Tuple undefined undefined of Tuple _ _ -> ()
+ ()
のように値コンストラクタは受け取った引数の評価を行わず,素直にそのままの形で遅延させて格納するため,エラーを出す式を渡してもその式の評価を行わない限りエラーにはならない.これは通常の関数の動作と同じになる.ところが,正格性フラグを使用した StrictTuple
の場合,
>>> case StrictTuple undefined undefined of StrictTuple _ _ -> ()
+*** Exception: Prelude.undefined
のように引数の評価を行うため,エラーを出す式を受け取った場合値コンストラクタの適用においてその式を評価しエラーを出す.データ型を作成する際,その元となる式の評価を強制させることはパフォーマンスに大きく寄与する.そのため,そのようなことを支援するために正格性フラグは設けられている.
+また,代数的データ型の値コンストラクタはフィールド名を持つことができる:
+data Tuple a b = Tuple
+ firstVal :: a
+ { secondVal :: b
+ , }
この場合,型コンストラクタ Tuple
,値コンストラクタ Tuple :: a -> b -> Tuple a b
の他に,関数 firstVal :: Tuple a b -> a
, secondVal :: Tuple a b -> b
が作られる.また,値コンストラクタの呼び出しにおいて特別なレコード構文 Tuple { firstVal = 0, secondVal = 1 }
を使用でき,またレコード更新構文 (Tuple 2 1) { firstVal = 0 }
を使用できる.これらは両者 Tuple 0 1
と同様の値が作成される.
さて,これまで見てきたように,型シノニムは型の別名を定義し,代数的データ型は型の積和により新たなデータ型を定義するものだった.Haskell にはもう1つデータ型を定義する方法がある.それが newtype
宣言だ.この宣言によって作られるデータ型は,型システム上は代数的データ型と同じように扱われ,実行時は型シノニムと同様の動作をする.
newtype
宣言の構文は,data
宣言と同じような形をしている:
newtype Identity a = Identity a
フィールド名をつけることもできる:
+newtype Identity a = Identity
+ unIdentity :: a
+ { }
この場合 data
宣言と同様に,型コンストラクタ Identity
,値コンストラクタ Identity
が作られることになる.ただし,data
宣言と異なり newtype
は積和の機能を使用することはできない.単にある1つの型を受け取る値コンストラクタしか定義できない.なので,
newtype Unit = Unit
+newtype Tuple a b = Tuple a b
+newtype Enum = A | B | C
はいずれも受け入れられない.この newtype
の制約はいまいちよく分からない.では,このような制約によりどのような違いが出るのだろうか? newtype
と data
は型システム上は違いはない.しかし,パターンマッチの動作など,実行時の動作に少し差異が設けられている.例えば,通常
data DataIdentity a = DataIdentity a
において,
+>>> case undefined :: DataIdentity () of DataIdentity _ -> ()
+*** Exception: Prelude.undefined
のようにエラーを出す式をパターンマッチで分解しようとするとエラーが出力される.ところが,newtype
によって作られた値コンストラクタの場合,
>>> case undefined :: Identity () of Identity _ -> ()
+ ()
のようにパターンマッチ時にエラーが出されることはない.Haskell では newtype
で作られた値コンストラクタが実行動作に影響することはないと規定されている.よって,上のパターンマッチは,以下と同様の動きをすることになっている:
>>> case undefined :: Identity () of _ -> ()
+ ()
このように値コンストラクタを指定しないパターンマッチの場合,data
宣言で作られたものもエラーを出さない:
>>> case undefined :: DataIdentity () of _ -> ()
+ ()
よって,data
と newtype
で作られた値コンストラクタの動作が異なるのは,パターンマッチにおいて値コンストラクタを指定した場合だけということになる.
では,newtype
はなぜ値コンストラクタを無視するよう規定されているのだろう? これは,newtype
によるデータ型が実行時の動作として型シノニムと同様の動作をすることを目的としてしているからだ.値コンストラクタが無視されるのは,
newtype Identity a = Identity a
という宣言は,
+type IdentitySynonym a = a
という宣言と同様の意味を持って欲しいことを Haskell の設計者が意図しているからだ.よって,
+>>> case undefined :: Identity () of Identity _ -> ()
+ ()
の動作は,
+>>> case undefined :: IdentitySynonym () of _ -> ()
+ ()
のように,代数的データ型ではなく型シノニムに合わせてあるため,data
宣言主体に見ると一見不思議な動作をしていたというわけだ.
さて,ではなぜわざわざ型シノニムとは別に newtype
宣言を導入したのだろうか? 型シノニムには幾つか制約があったのを思い出して欲しい.そして,それらの制約は代数的データ型では解決されたのだった.それは type
宣言が単に型の別名を導入するのに対し,data
宣言が完全に新たな型を作るからだった.newtype
はその点に着目し,実行時には単なる別名として動作するが型システム上は完全に別の新たな型を導入することで,type
宣言同様ある型の別名を作りたいものの型シノニムの制約は回避したい需要を満たすようにしたものだ.
例えば,大文字小文字を区別しない文字列データを考えてみよう.この場合,"aBc" == "Abc"
であって欲しいが,これは型シノニムで
type CaseInsensString = String
と定義するだけでは,
+>>> ("aBc" :: CaseInsensString) == ("Abc" :: CaseInsensString)
+False
のままだ.そこで,newtype
を使って,
import qualified Data.Char as Char
+
+newtype CaseInsensString = CaseInsens String
+
+instance Eq CaseInsensString where
+CaseInsens s1 == CaseInsens s2 = go s1 s2
+ where
+ = True
+ go [] [] :_) = False
+ go [] (_:_) [] = False
+ go (_:cs1) (c2:cs2) = Char.toLower c1 == Char.toLower c2 && go cs1 cs2 go (c1
とすれば,
+>>> CaseInsens "aBc" == CaseInsens "Abc"
+True
とできる.型シノニムは単なる String
の別名なので,String
と異なるインスタンスを新しく定義することはできない.それに対して,newtype
によるデータ型は代数的データ型と同様に自由に定義することができる.そして,値コンストラクタ CaseInsens
は単なる飾りであり,実行時には完全に無視されるため,CaseInsensString
は動作としては String
の別名としてみることができる.
newtype
は型シノニムでの制約であった,
といった問題も解決する.このように newtype
は型シノニムの問題を改善したデータ型を定義するが,data
宣言と同様型シノニムでは起きなかった問題も一緒に顕在化させてしまう.
上の例で,CaseInsens
は飾りだと言ったが,実際にはこの値コンストラクタは必要不可欠であり,重要な役割を持っている.例えば,
>>> CaseInsens "aBc" == CaseInsens "Abc"
+True
の例は,片方だけ
+>>> "aBc" == CaseInsens "Abc"
としてしまうと,コンパイルエラーになってしまう.なぜなら,(==)
は2つの引数が同じ型の値である必要があり,"aBc"
の型である String
と CaseInsens "Abc
の型である CaseInsensString
は全く異なる型であるからだ.つまり,値コンストラクタ CaseInsens
は,実行時には何の影響も与えないが,型システム上は全く異なる型の値であることを示すマーカーとなる.そして,型シノニムではデータ型は単なる別名であったが,newtype
は data
と同様全く新たな型として導入する道を選んだため,元の型として受け入れてもらうことが出来なくなってしまったのだ.
といっても,これは一長一短である.data
と同様 newtype
で作られた型は,型シノニムのように既存の関数を使い回すことができない.その反面,データの意味に沿わないプログラムを型によって弾くことができるという点は長所になる場合もある.例えば,"aBc" == CaseInsens "Abc"
の例は,一体どのような結果を返すべきか一見して分からない.両者は単なる文字列と,大文字小文字を区別しない文字列という異なるデータを表しており,その比較は定義されないとするのが自然だろう.このような場合に,型シノニムでは定義されないことを表す方法はなかったが,newtype
は元の型と異なる型を持つので,そのような仕組みを作ることができる.
さて,newtype
において値コンストラクタは実行時に何の影響も及ぼさないことと,何故そうなっているかについて分かってもらえただろうか? この影響は,パターンマッチ以外にも表れる.例えば,newtype
の値コンストラクタに正格性フラグの機能はない.
newtype StrictNewtype = StrictNewtype !Int
というプログラムは,Haskell では受け入れられない.なぜなら,これを受け入れた場合,値コンストラクタがあるかどうかによって実行時の動作が変わってしまうからだ.ただ,その他の data
宣言の機能は使用できる.deriving
も使用できる.newtype
で作られたデータ型は,元のデータ型のインスタンスを継承することはできない.全く新たな型を作ったため,更地の状態から始まる.ただし,deriving
を使うことでインスタンスを用意に導出することは可能だ.ただ,標準クラスのインスタンスしか自動で導出できないため,自身で定義した型クラスなどのインスタンスは一から書く必要がある.そのことには,注意する必要があるだろう 7.
最後に少し応用的な newtype
の使い方を紹介しよう.newtype
は上のように目的に合わせて型を既存の型から作る他,型シノニムの制約によって定義できない型上の計算を実現するのにも使用できる.例えば,
newtype Fix f = Fix (f (Fix f))
という変わったデータ型を使うと,型上の不動点演算をエミュレートできる.また,newtype
を使うことで幽霊型による曖昧な型を避けることもできる.例えば,
type WithAnn ann a = a
+
+readShow :: (Read a, Show a) => WithAnn a String -> String
+= show $ read s readShow s
を考える.この関数 readShow
は,WithAnn
で引数に a
を使っているにもかかわらず a
が曖昧な型になるため弾かれる.なぜなら,型シノニム WithAnn a String
は String
と書いてるのと同じであり,readShow
は
readShow :: (Read a, Show a) => String -> String
という型を持つのと同様になってしまうからだ.このため,制約だけに a
が現れることになってしまい,曖昧な型になってしまう.この例のような,型シノニムが具体化されてしまうことで曖昧な型が生じる問題は,newtype
を使用することで回避できる:
newtype WithAnn ann a = WithAnn a
+
+readShow :: (Read a, Show a) => WithAnn a String -> String
+WithAnn s) = show $ read s readShow (
Haskell は型システム上は WithAnn a String
が実行時に単なる String
の別名として扱われることを知らず,これを1つの具体化された型として認識する.このため,実際には a
が引数の値に何ら関与しない場合も,型 a
を伴う型として残る.よって,この場合は a
は曖昧な型にならず,WithAnn a String
の a
の部分にあてがわれる型から特定することができる.このように,型シノニムで早期に元となった型に具体化されることで生じる問題は,newtype
を使うことで実際に値を作る箇所とパターンマッチの箇所での型計算に遅延させることができ,回避できる場合がある.
Haskell の3つのデータ型定義方法について紹介した.
+型シノニムは,ある型に対してその別名を与えることで,データ型を定義するものだった.簡易で元の型に対する関数をそのまま流用でき,使いやすい反面,部分適用ができない,再帰的データ型が定義できない,型クラスのインスタンスにできないと言う制約があった.
+代数的データ型は複数の型の積和によって全く新しいデータ型を定義するものだった.型シノニムであった制約を回避でき,新たな構造を導入できるが,関数の流用が困難な場合があり型シノニムとの使い分けが必要だった.
+newtype
によるデータ型は,型システム上は代数的データ型と,実行時の動作は型シノニムと同様といった,それぞれの中間をとったようなものだった.型シノニムのような関数の流用ができない場合はあるものの,その代わり型シノニムの制約を回避でき,型システム上は全く異なる振る舞いを行うことも可能だった.
これらは,それぞれが一長一短を持ち,目的にあった使い分けをする必要がある.この記事が,そのような場合の助けになればいいと思う.では,今回はこれで.
+型シノニムに対して部分適用を許容する一般的な方法は,型上にもラムダ抽象にあたる表現を導入することである.ただ,この場合型上の演算が停止しない場合があり,型システムが決定不能になる.このため,Haskell では型シノニムに対しての部分適用は許容していない.↩︎
等価再帰データ型 (equirecursive types) と呼ばれる特別な型を型システムに導入することで,このような型を許容する理論は存在するが,この理論はとても複雑で型検査のアルゴリズムも難しくなりがちである.↩︎
ただ,このような混乱が起こるかもしれないことを許容し,利便性のため型シノニムをインスタンス定義で使いたい場合,TypeSynonymInstances
という GHC 拡張を有効にすることで許容されるようになる.↩︎
型上の計算によって,実際の型が特定される型シノニムとは異なり,代数的データ型の型コンストラクタはそれ自体がもう計算できないものになる.それは部分適用されても同様であり,部分適用を許容することで型シノニムと同様の問題は起こらない.これが,代数的データ型で部分適用が許容されている理由になる.↩︎
実際にはパラメータ a
の部分に具体的な型を当てはめないといけないが,当てはめればそれは完全に具体的な型になる.↩︎
なお,代数的データ型でも型シノニムと同様の利点を手に入れるための研究は,Haskell では盛んに行われている.例えば,Generic
/ Data
型クラス,lens
パッケージなどを使うことで,構造が同じだが異なるデータ型で関数が流用できない問題を回避できる場合がある.↩︎
GHC 拡張では,deriving
構文の拡張として強力な機能がいくつか搭載されている.特に newtype
によるデータ型の場合は,GeneralizedNewtypeDeriving
や DerivingVia
拡張を使えば,インスタンスの自動導出の範囲を大幅に拡大できる.↩︎
Haskellは他の多くのプログラミング言語と異なった特徴を備えており、しばしばそれらが議論を呼ぶことがあります。その中でも特によく俎上に上がるのが、遅延評価です。遅延評価は、適切に扱えば不要な計算を行わず、計算資源を節約してくれるステキな仕組みですが、一歩使い方を間違うと「サンク」という「これから実行する(かも知れない)計算」を表すオブジェクトが無駄に作られてしまい、却ってメモリー消費量が増えてしまう、などといった問題を抱えています。この現象は「スペースリーク」と呼ばれ、かつて専門のAdvent Calendarが作られたことがあるほど、Haskeller達の関心を集めてきました。
+そんなHaskeller達の悩みの種を軽減しようと、GHC 8.0以降、Strict
とStrictData
という言語拡張が搭載されました。これらの拡張は、大雑把に言うと、
StrictData
: 値コンストラクターにおいて、引数の値が弱頭正規形(Weak Head Normal Form。以降慣習に従い「WHNF」と呼びます)まで評価されるようになるStrict
: StrictData
の効果に加え、あらゆる関数の引数やローカル変数の定義において、パターンマッチで代入した変数の値がWHNFまで評価されるようになるというものです。
+このうち、StrictData
は比較的リスクが少なく大変有用(もはや標準であって欲しいぐらい)という声をよく聞きますが1、Strict
については様々な問題点があることが知られています。今回はその各種問題点をまとめて共有することで、思い切ってStrict
を有効にするときに参考になる情報を提供したいと思います!
以下の知識について、ざっくり理解しているものとして進めます。参考になりそうな日本語のページも付記したので、ご覧ください。
+BangPatterns
について
+Strict
とStrictData
について
+これから紹介するコードは、すべてこのブログのリポジトリーの、examples
ディレクトリーに置いておきました。下記のコマンドを実行すれば実際に試すことができます(一部実行する際のコマンドが異なりますので、適宜例示します)。
git clone https://github.com/haskell-jp/blog.git
+cd blog/examples/2020/strict-gotchas
+stack exec runghc -- <これから紹介するコードのファイル>.hs
+実際に試すときは--ghc-arg=-XStrict
というオプションをrunghc
に付けた場合と付けなかった場合両方で実行して、違いを確かめてみてください。
なお、使用したGHCのバージョンは8.10.1で、OSはWindows 10 ver. 1909です。
+where
句だろうとなんだろうと評価サンプル: where.hs
+最初のケースは、遅延評価で当たり前に享受できていたメリットが、Strict
を有効にしている状態では得られなくなってしまう、というものです。pxfncさんのStrict拡張でハマったお話という記事でも紹介されてはいますが、まとめ記事なのでここでも改めて取り上げます。
= print $ div10 0
+ main
+div10 :: Int -> Int
+
+ div10 n| n == 0 = 0
+ | otherwise = result
+ where
+ = 10 `div` n result
ご覧のとおり、本当にほとんどpxfncさんの記事のサンプルそのままで恐縮ですが、このプログラム、👇のようにStrict
拡張を有効にして実行するとエラーが起こります。
> stack exec -- runghc --ghc-arg=-XStrict where.hs
+where.hs: divide by zero
一方、Strict
拡張を有効にしなかった場合、エラーは起こりません。
> stack exec -- runghc where.hs
+0
なぜこんなことが起こるのでしょう?
+これは、Strict
拡張がパターンマッチで代入したあらゆる変数の値をWHNFまで評価するようになった結果、where
句で代入した変数まで必ずWHNFまで評価してしまうために発生したエラーです。すなわち、where
における、
= 10 `div` n result
までもが、
+!result = 10 `div` n
とBangパターンを付けた代入であるかのように解釈されたのです2。
+こうなると、result
を使用しないケース、すなわちn == 0
の場合であってもresult
に (WHNFまで評価した)値を代入するのに必要な計算は実行され、結果10 `div` 0
が計算されようとしてdivide by zero
が発生するのです。
⚠️where
句は関数定義の後ろの方に書くという性格上、見落としがちかも知れません。注意しましょう。
サンプル: const.hs
+続いて、Haskellに慣れた方なら誰もが一度は試したくなる、ポイントフリースタイルに関する落とし穴です。まずは次の二つの関数をご覧ください。
+dontReferArgs :: a -> b -> a
+= const
+ dontReferArgs
+referArgs :: a -> b -> a
+= x referArgs x _
この関数、どちらもやっていることはconst
と変わりません。dontReferArgs
はconst
をそのまま使うことでポイントフリースタイルにしていますが、referArgs
は自前で引数に言及することでconst
と同等の定義となっています。ポイントフリースタイルに変えると言うことは原則として元の関数の挙動を変えないワケですから、dontReferArgs
とreferArgs
の意味は変わらないはず、ですよね3?
ところがこれらの関数をStrict
拡張を有効にした上で定義すると、なんと挙動が異なってしまいます!
使用例:
+main :: IO ()
+= do
+ main print $ dontReferArgs "dontReferArgs" (undefined :: Int)
+ print $ referArgs "referArgs" (undefined :: Int)
実行結果(Strict拡張を有効にしなかった場合):
+> stack exec runghc const.hs
+"dontReferArgs"
+"referArgs"
実行結果(Strict拡張を有効にした場合):
+> stack exec -- runghc --ghc-arg=-XStrict const.hs
+"dontReferArgs"
+const.hs: Prelude.undefined
+CallStack (from HasCallStack):
+error, called at libraries\base\GHC\Err.hs:79:14 in base:GHC.Err
+ undefined, called at const.hs:10:34 in main:Main
はい、where
句のケースと同様、Strict
拡張を有効にした場合、例外が発生してしまいました❗️Strict
拡張を有効にした結果、意図せず例外を発生させる値(今回の場合undefined
)が評価されてしまったのです。
例外を発生させた関数はそう、ポイントフリースタイルでない、referArgs
関数の方です!なぜreferArgs
でのみ例外が発生してしまったのかというと、referArgs
がStrict
拡張を有効にしたモジュールで、引数に言及(パターンマッチ)しているからです。Strict
拡張を有効にした結果「あらゆる関数やローカル変数の定義において、パターンマッチで代入した変数の値」が評価されるとおり、referArgs
の引数x
・_
も必ず評価されるようになり、このような例外が発生したのです。たとえ使用しない変数_
でも関係ありません!
そのため、原因の本質は引数に言及(パターンマッチ)しているか否かであり、Prelude
のconst
を使用しているか否かではありません。こちら👇のように引数に言及した上でconst
を使っても、結果は同じなのです。
referArgsByConst :: a -> b -> a
+= const x y referArgsByConst x y
print $ referArgsByConst "referArgsByConst" (undefined :: Int)
一方、dontReferArgs
については、引数に言及せず、Prelude
にあるconst
をそのまま使っています。Strict
拡張はあくまでも「パターンマッチした変数」のみをWHNFまで評価するものであり、あらゆる関数が正格に呼び出されるわけではありません。なので通常のPrelude
におけるconst
と同様、dontReferArgs
も第2引数は評価しないため、undefined
を渡しても例外は起こらなかったのです。
このことは、「Strict
拡張を有効にしているモジュールの中でも、Strict
を有効にしていないモジュール(この場合はPrelude
)からimport
した関数は、引数を正格に評価しない」という忘れてはならないポイントも示しています。例えばconst
よりももっと頻繁に使われるであろう、言及する引数を一つ削除する演算子の代表、関数合成.
を使ったケースを考えてみてください。
ポイントフリースタイルに慣れた方なら、関数適用$
を次👇のように使って定義したf
を見ると、
= map (+ 3) $ filter (> 2) xs
+ f xs
+-- あるいは、`$`を使わないでこのように書いた場合も:
+= map (+ 3) (filter (> 2) xs) f xs
こちら👇のように書き換えたくなってうずうずするでしょう。
+= map (+ 3) . filter (> 2) f
しかし、Strict
を有効にしたモジュールでこのような書き換えを行うと、f
の挙動が変わってしまいます。引数.
を使って書き換える前は、引数xs
に言及していたところ.
を使って引数xs
に言及しなくなったからです。filter
もmap
もStrict
拡張を有効にしたモジュールで定義されているわけではないので、引数を正格に評価しないんですね。結果、こうした書き換えによって、Strict
拡張を有効にしていても意図せず遅延評価してしまう、というリスクがあるので、リファクタリングの際はくれぐれも気をつけてください4。ざっくりまとめると、Strict
拡張を有効にしているモジュールでは、「引数や変数を宣言することすなわちWHNFまで評価すること」、あるいは「引数や変数を宣言しなければ、評価されない」と意識しましょう。
ちなみに、referArgs
における_
のように「Strict
拡張を有効にした場合さえ、使用していない引数が評価されてしまうのは困る!」という場合は、引数名の前にチルダ~
を付けてください。
referArgs :: a -> b -> a
+~_ = x referArgs x
サンプル: 今回はGHCiですべて紹介するのでサンプルはありません。
+続いては、Strict
拡張のドキュメントでも触れられている、入れ子になったパターンマッチにおける問題を紹介します。一言で言うと、let (a, b) = ...
のような、データ構造(この場合タプルですね)の「内側」に対するパターンマッチは、Strict
拡張を有効にしていても正格に評価しないよ、という話です。
例えば、下記のコードをStrict
拡張付きで実行しても、パターンマッチしているa
・b
ともに代入した時点では正格評価されず、error "a"
・error "b"
による例外はいずれも発生しません。次のコードをGHCiで試してみてください。
> :set -XStrict
+> (a, b) = (error "a", error "b")
+-- 何も起きない
先ほどの節における「Strict
拡張を有効にしているモジュールでは、『引数や変数を宣言することすなわちWHNFまで評価すること」』、あるいは『引数や変数を宣言しなければ、評価されない』と意識しましょう」という主張を真に受けてしまうと、意図せず遅延評価させてしまい、ハマりそうです😰。⚠️繰り返しますが「内側のパターンにおける変数は正格評価されない」ということも意識してください。
一方、StrictData
や正格性フラグを用いるなどして、各要素を正格評価するよう定義した値コンストラクターでは、ちゃんと評価して例外を発生させます。
> :set -XStrict
+> data MyTuple a b = MyTuple a b deriving Show
+> let MyTuple a b = MyTuple (error "a") (error "b")
+*** Exception: b
+CallStack (from HasCallStack):
+error, called at <interactive>:10:40 in interactive:Ghci7
Strict
拡張を有効にするとStrictData
も自動的に有効になるので、👆におけるMyTuple
値コンストラクターは各要素を正格評価するようになったわけです。なのでStrict
拡張を有効にしたモジュールにおいて、なおかつそこで定義した型で完結している限りは平和でしょう。
ただし、GHCiで試す場合に特に注意していただきたいのですが、GHCiでlet
をつけないでパターンマッチした場合は正格評価されない、という点に注意してください。let
をつけないとトップレベルでの定義と見なされるからです。Strict拡張のドキュメントにも、「Top level bindings are unaffected by Strict
」とありますとおり、トップレベルでの定義は例外扱いされているのです。
> :set -XStrict
+> data MyTuple a b = MyTuple a b deriving Show
+> MyTuple a b = MyTuple (error "a") (error "b")
+-- 何も起きない
foldr
に渡す関数サンプル: stackoverflow-foldr.hs
+ここの話はちょっと難しいので、先に守るべきルールを述べておきます。
+「遅延評価する関数を受け取る前提の高階関数に、(Strict
拡張などで)引数を正格に評価するよう定義された関数を渡すのは止めましょう。」
なんだかこう書くと半ばトートロジーのようにも聞こえますが、より具体的には、例えばfoldr
に引数を正格に評価するよう定義された関数を渡すのは止めましょう、という話です。Strict
拡張を有効にした状態では、ラムダ式にも注意しないといけないもポイントです。
※あらかじめおことわり: この節のお話は、あくまでもリストに対するfoldr
の場合のお話です。他のFoldable
な型では必ずしも当てはまらないのでご注意ください。
論より証拠で、サンプルコードの中身(抜粋)とその実行結果を見てみましょう。
+-- ...
+evaluate . length $ foldr (:) [] [1 .. size]
+putStrLn "DONE: foldr 1"
+
+evaluate . length $ foldr (\x z -> x : z) [] [1 .. size]
+putStrLn "DONE: foldr 2"
+今回のサンプルコードを実行する際は、GHCのランタイムオプションを設定して、スタック領域のサイズを減らしてください。そうでなければ、処理するリストがあまり大きくないのでStrict
拡張を有効にしても問題の現象は再現されないでしょう5。こちらのStackoverflowの質問曰く、runghc
で実行する際にランタイムオプションを設定する場合は、GHCRTS
環境変数を使用するしかないそうです。
実行結果(Strict拡張を有効にしなかった場合):
+> GHCRTS=-K100k stack exec runghc -- ./stackoverflow-foldr.hs
+DONE: foldr 1
+DONE: foldr 2
実行結果(Strict拡張を有効にした場合):
+> GHCRTS=-K100k stack exec runghc -- --ghc-arg=-XStrict ./stackoverflow-foldr.hs
+DONE: foldr 1
+stackoverflow-foldr.hs: stack overflow
サンプルコードは整数のリストに対して特に何も変換せずfoldr
する(そして、length
関数でリスト全体を評価してから捨てる)だけのことを2回繰り返したコードです。最初のfoldr
はStrict
拡張があろうとなかろうと無事実行できたにもかかわらず、Strict
拡張を有効にした二つめのfoldr
は、stack overflow
というエラーを起こしてしまいました💥!
なぜこんなエラーが発生したのかを知るために、foldr
の定義を見直しましょう。こちら👇はGHC 8.10.1における、リストに対するfoldr
の定義です(コメントは省略しています)。
foldr :: (a -> b -> b) -> b -> [a] -> b
+foldr k z = go
+where
+ = z
+ go [] :ys) = y `k` go ys go (y
go
という補助関数を再帰的に呼び出すことで、第一引数として渡した関数k
を用いてリストの要素(y
)を一つずつ変換しています。呼び出す度にリストの残りの要素をチェックして、最終的に空のリストを受け取ったときはfoldr
の第二引数z
を返していますね。
このときk
が第二引数を遅延評価する関数であった場合、 — サンプルコードで言えば(:)
の場合 — 受け取ったgo ys
という式は直ちには評価されません。サンプルコードの(:)
に置き換えると、(:)
の第二引数、つまりリストの残りの要素を取り出す度にgo ys
を一回計算して、一個ずつ要素を作り出すイメージです。
一方、k
が第二引数を正格評価する関数であった場合、 — サンプルコードで言うところの、Strict
拡張を有効にした(\x z -> x : z)
の場合 — k
は受け取ったgo ys
をすぐに評価しようとします。このとき、GHCはk
やgo
に渡されている引数をスタック領域に積みます6。そうしてgo
と、go
に呼ばれたk
が次々と引数をスタック領域に積んだ結果、スタックサイズの上限に達し、スタックオーバーフローが発生してしまうのです。
これは他の多くのプログラミング言語で(末尾再帰じゃない、普通の)再帰呼び出しを行った場合とよく似た振る舞いです。間違って無限再帰呼び出しをしてしまってスタック領域があふれる、なんて経験は多くのプログラマーがお持ちでしょう。つまり単純に、Strict
拡張を有効にした場合のfoldr (\x z -> x : z) []
は、再帰呼び出しをしすぎてしまう関数となるのです。
なお、今回はlength
関数を使ってリスト全体を使用するコードにしましたが、遅延リストらしくfoldr
の結果を一部しか使わない、という場合、foldr
に渡した関数がリストを都度正格評価してしまうので、無駄な評価が占める割合はもっと増えることになります。やはりfoldr
は遅延評価を前提とした高階関数と言えるでしょう。
以上のとおり、Haskellにはfoldr
のような、遅延評価を前提とした関数がStrict
拡張より遥か昔から存在しています。それらをStrict
拡張を有効にした状態で使うと、思わぬ衝突が起きてしまうので、くれぐれも気をつけましょう。
こういう「使ってはいけない関数」を引いてしまわないための方法についても一点補足します。HLintを細かく設定したり、カスタムPrelude
を設定したりしてみるのは、一つの作戦です。なんとプロジェクト全体で、foldr
を禁止することができます(一部のモジュールでは例外的に許可する、なんてこともできます)。詳しくは「素晴らしき HLint を使いこなす」や「Prelude を カスタムPrelude で置き換える」をご覧ください。
undefined
を受け取るメソッドサンプル: storable.hs
+最後はちょっとレアケースではありますが、こちら👇のIssueで発覚した問題を解説しましょう。
+ +問題を簡単に再現するために、次のサンプルコードを用意しました。
+-- importなどは当然省略!
+data Test = Test Int Int deriving Show
+
+instance Storable Test where
+= sizeOf (1 :: Int) * 2
+ sizeOf _ = 8
+ alignment _ = error "This should not be called in this program"
+ peek = error "This should not be called in this program"
+ poke
+= alloca $ \(_ :: Ptr Test) -> putStrLn "This won't be printed when Strict is enabled" main
はい、適当な型を定義してStorable
のインスタンスにして、それに対してalloca
を呼ぶだけのコードです。インスタンス定義をはじめかなり手抜きな感じになっちゃってますが、まぁ今回の問題を再現するのにはこれで十分なので、ご了承ください🙏。
このコード、残念ながらStrict
拡張を有効にした状態で実行すると、undefined
による例外が発生してしまいます💥。
> stack exec -- runghc --ghc-arg=-XStrict storable.hs
+storable.hs: Prelude.undefined
+CallStack (from HasCallStack):
+error, called at libraries\base\GHC\Err.hs:79:14 in base:GHC.Err
+ undefined, called at libraries\base\Foreign\Marshal\Alloc.hs:117:31 in base:Foreign.Marshal.Alloc
こちらはStrict
を有効にしなかった場合。やはり例外は起きてませんね😌。
> stack exec -- runghc storable.hs
+This won't be printed when Strict is enabled
さてこの、Strict
拡張を有効にした場合に発生した、undefined
による例外はどこからやってきたのでしょう?上記のコードにはいくつかerror
関数を使用している箇所がありますが、発生した例外はあくまでもundefined
です。見た限り上記のコードそのものから発生した例外ではなさそうですね…🤔。
その答えはなんと、main
関数で呼んでいるalloca
の定義にありました!
alloca :: forall a b . Storable a => (Ptr a -> IO b) -> IO b
+=
+ alloca undefined :: a)) (alignment (undefined :: a)) allocaBytesAligned (sizeOf (
確かに、sizeOf
メソッドやalignment
メソッドにundefined
を渡しています。これらはいずれもStorable
型クラスのメソッドなので、上記のTest
型でももちろん実装しています。そう、実はこのsizeOf
メソッドとalignment
メソッドの実装で、下👇のように引数_
を宣言しているのが問題なのです!
instance Storable Test where
+= sizeOf (1 :: Int) * 2
+ sizeOf _ = 8
+ alignment _ -- ...
「Case 2: ポイントフリースタイルかどうかで変わる!」の節で、「Strict
拡張を有効にしているモジュールでは、『引数や変数を宣言することすなわちWHNFまで評価すること」』、あるいは『引数や変数を宣言しなければ、評価されない』」と述べたことを再び思い出してください。こちらのsizeOf
・alignment
の定義でも同様に、引数_
を宣言しているため、引数を必ずWHNFまで評価することになっています。結果、alloca
関数がそれぞれを呼ぶ際undefined
を渡しているため、undefined
を評価してしまい、undefined
による例外が発生してしまうのです💥。
なぜこのように、alloca
関数ではsizeOf
やalignment
にundefined
をわざわざ渡しているのでしょう?それは、これらのメソッドがそもそもundefined
を渡して使うことを前提に設計されているからです。sizeOf
・alignment
はともにStorable a => a -> Int
という型の関数なので、第一引数にStorable
のインスタンスである型a
の値を受け取るのですが、このとき渡されるa
型の値は、使わないこととなっています。それぞれのメソッドの説明にも「The value of the argument is not used.」と書かれていますね。これは、sizeOf
もalignment
も、型毎に一意な値として定まる(引数の値によってsizeOf
やalignment
の結果が変わることがない)ので、第一引数のa
は、単に「この型のsizeOf
を呼んでくださいね」という型の情報を渡すためのものでしかないからです。だから値には関心がないのでundefined
を渡しているわけです。そもそも、alloca
関数のように引数としてStorable a => a
型の値をとらない関数では、a
型の値を用意することができませんし。
現代では通常、このように「値に関心がなく、何の型であるかという情報だけを受け取りたい」という場合は、Proxy
型を使うのが一般的です。Storable
は恐らくProxy
が発明される前に生まれたため、undefined
を渡すことになってしまっているのでしょう。なので、Storable
型クラスのインスタンスを自前で定義したりしない限り、こうしたケースに出遭うことはまれだと思います。ただ、それでもProxy
をimport
するのを面倒くさがってundefined
を代わりに渡す、なんてケースはありえるので、Proxy
を使って定義した型クラスでも同じ問題にハマることはあるかも知れません…。
⚠️結論として、Storable
型クラスや、Proxy
を受け取るメソッドを持つ型クラスのインスタンスを、Strict
拡張を有効にした状態で定義する場合は、Proxy
にあたる引数を評価しないよう、~_
などを使って定義しましょう。
Strict
は使う?使わない?さて、ここまでStrict
拡張を有効にすることによって犯しうる、数々のミスを紹介してきました。ここまで書いた個人的な印象としては、「敢えて有効にする必要はないんじゃないか」といったところです(まぁ、悪いところばかり調べた結果のため、とてもフェアな視点での判断とは言えないのですが…)。foldr
の例でも触れたとおり、Haskellには遅延評価を前提とした、遅延評価を存分に活かした機能が溢れています。当然それらはStrict
拡張ができるよりはるか昔からあり、Strict
拡張のことなど一切考えないで作られたものです。動的型付け言語に後から静的型検査を導入するのが大変なように、相対する機能を後付けすると衝突が起こるのは仕方のないことですが、ことStrict
拡張については想像以上に大きな衝突のようです😞。
それでも使いたいという方に、今回の記事が助けになれば幸いです💪それではStrict
な方もNoStrict
な方もHappy Haskell Hacking!!
例えばfumievalさんによるこの記事より: 「もっとも、日常ではここまで気にしなければいけない場面は少ないので、ほとんどの場合は気にせず感嘆符をつけて大丈夫だろう。GHC 8.0からは、全フィールドをデフォルトで正格にするStrictData
という拡張が入るため、こちらを使おう」↩︎
BangPatterns
言語拡張を有効にした上で上記のように書き換えてみると、Strict
拡張の有無に関わらずエラーが発生します。試してみましょう。↩︎
実際のところ今回紹介するケース以外にも、ポイントフリースタイルにするかしないかで実行効率などが変わる場合があります。例えば、Evaluation of function calls in Haskellをご覧ください。↩︎
もっとも、この例では引数はリストでしょうから、WHNFまでのみ正格評価するメリットは少なそうですが。↩︎
大きなリストにすると、今度はエラーが発生するまでに時間がかかってしまうので…。ちなみに、このようにスタック領域を小さくすることでスペースリークを検出する手法は、ndmitchell/spaceleak: Notes on space leaksでも紹介されています。↩︎
GHCがどのように評価し、スタック領域を消費するかはGHC illustratedや、その参考文献をご覧ください。↩︎
はじめまして。Haskell-jpこと日本Haskellユーザーグループです!
+この度、日本におけるHaskellの普及を目指して、ユーザーグループを立ち上げることといたしました。
+詳しいことは「日本Haskellユーザーグループについて」をご覧いただくとして、立ち上げにともない、3点発表です!
Haskell-jp立ち上げ前、有志による議論に使用していたSlackチームを開放します!
+下記から登録してください!
https://haskell.jp/signin-slack.html
+現時点の運用ルールは、以下のとおりです。
+Haskell-jp発起人の一人である@igrepが主催していた「Haskellもくもく会」を、「Haskell-jpもくもく会」、すなわちわれわれ日本Haskellユーザーグループの活動の一部として再出発させます!
+といっても、実態は今まで通りです。会場もやることもハッシュタグも特に変わりありません。
+これからもよろしくお願いします!
Haskell-jpはHaskellを普及させる(多くの人に使ってもらう)ためのグループであり、主役はHaskellユーザーのみなさんです。
+そこで、Haskellユーザーのみなさんがより効果的にHaskellを広められるよう、当blogで掲載する記事の寄稿を募集します!
募集する記事のテーマは次のとおりです。
+応募していただける場合、こちらのリポジトリーにPull Requestを送ってください。
+preprocessed-site/posts
というディレクトリー以下に新しいmarkdownファイルを追加していただく形になります。
+こちらの記事のテンプレートも参考にしてください。
+なにか気になる点があればSlackチームの#haskell-jp-blogチャンネルまでお気軽にどうぞ!
+(2017年9月30日修正: チャンネル名を実態に合わせて修正しました)
早速最初の記事として、@arowmさんに「Dockerを使ってHaskellアプリをHerokuにデプロイする」という記事をいただきました!ありがとうございます!
+これまで、HaskellのコードをHerokuで実行しようとすると、コンパイルがHerokuの制約時間内に終わらず、面倒なハックが必要でつらい状態でした。 +でも、HerokuがDockerをサポートするようになった今なら、Haskell製のウェブアプリケーションをHeroku上で公開するのはずっと簡単です。 +この記事では、Servant(HaskellのWebフレームワークの1つ)で作ったアプリケーションを、Dockerの力を借りてHerokuにデプロイする方法について、具体的なプログラムを使って順を追って説明していきます。
+この記事は、Releasing a Haskell Web App on Heroku with DockerとしてHaskell-jpオフィシャルスポンサーである株式会社ARoW公式ブログに公開されている英語の記事を、許可を得て日本版にローカライズしたものです1。
+今回、実際にHerokuにデプロイして試せるように、サンプルアプリを用意しました。 +この記事の最初の章では、このサンプルアプリをローカル環境で動かす方法について述べます。 +「今度はDockerをつかってアプリを動かしてみよう!」では、同じくローカル環境において、Dockerを使って動かす方法について触れます。 +「Herokuで動かす」で、ついにHerokuにこのサンプルアプリをHerokuにデプロイする方法についてお伝えします。
+もし、ローカル環境で動かしたりするのが面倒で、「いきなりHerokuにデプロイしたい!」という方は、 +「Herokuで動かす」から読んでいただいても問題ないように構成しているつもりです。
+今回用意したサンプルアプリは、以下の通りAPIを2つだけ提供する、とても単純なものです。
+このサンプルアプリでは、コメントを保存するのにPostgreSQLを利用しています。
+では、まずはDockerやHerokuをつかわないで、実際にローカルな環境でこのアプリをビルドして実行する手順を追っていきましょう。
+まず最初に、このサンプルアプリを公開しているgithubレポジトリをcloneして、アプリをビルドしてみましょう。
+$ git clone https://github.com/cdepillabout/servant-on-heroku
+$ cd servant-on-heroku/
+$ stack setup # このアプリが使っているバージョンのGHCをインストールします
+$ stack build # 依存パッケージをインストールし、ビルドします
もしかしたら、PostgreSQLのライブラリが入っていなくて、ビルドに失敗してしまうかもしれません。
+Arch Linuxの場合は、以下のコマンドで必要なライブラリをインストールできます。
+$ sudo pacman -Ss postgresql-libs
Ubuntuユーザの方は、以下のコマンドで大丈夫です。
+$ sudo apt-get install libpq-dev
上記以外のプラットフォームでは別のコマンドを使うことになると思うので、いい感じにググってください。
+では、PostgreSQLの必要なライブラリを入れたところで、stack build
をもう一度試してみましょう。今度はうまくいきましたよね?
うまくビルドできたら、アプリの実行をしてみます。
+$ stack exec -- servant-on-heroku-api
わーお!なにかエラーが出ちゃいますね…
+servant-on-heroku-api: libpq: failed (could not connect to server: Connection refused
+ Is the server running on host "localhost" (::1) and accepting
+ TCP/IP connections on port 5432?
+ could not connect to server: Connection refused
+ Is the server running on host "localhost" (127.0.0.1) and accepting
+ TCP/IP connections on port 5432?
+)
+サンプルアプリがPostgreSQLに接続しようとして失敗しているようです。 +このアプリは、コメントをPostgreSQLに保存しているので、PostgreSQLがローカルな環境で動いていないと、うまく動きません。
+お使いの環境によって、PostgreSQLのインストール方法はまちまちなので、 +そのプラットフォームが提供しているドキュメントにしたがって、PostgreSQLのインストールを行ってください。
+たとえば、Arch Linuxの場合はこのドキュメントです。 +Ubuntuならここにドキュメントがあります。
+さて、PostgreSQLをインストールして、動いているのが確認できたら、もう一度アプリを起動してみましょう。
+$ stack exec -- servant-on-heroku-api
わーお… またもやエラーです…
+servant-on-heroku-api: libpq: failed (FATAL: role "mydbuser" does not exist
+)
+どうやら、このサンプルアプリ用に、PostgreSQLのユーザとデータベースを用意しないといけないようですね。
+実際にサンプルアプリのソースコード(src/Lib.hs
)を見てみると、DATABASE_URL
という環境変数の値を見てPostgreSQLサーバに接続しているのがわかります。
DATABASE_URL
環境変数が指定されていない場合は、以下のデフォルト値が使われます。
postgres://mydbuser:mydbpass@localhost:5432/mydb
+mydbuser
というユーザ名で、mydbpass
というパスワードを使ってmydb
という名前のデータベースにアクセスしようとしているということですね。
+では、実際にこのユーザとデータベースをPostgreSQLで作成してみましょう。
+次のコマンドはArch Linuxでしか動かないかもしれません。
+もし動かないようであれば、お使いのプラットフォームが提供するドキュメントを参照してください。
最初に、mydbuser
という名前のユーザを、mydbpass
というパスワードで作成しましょう。
$ sudo -u postgres -- psql --command "CREATE ROLE mydbuser NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN ENCRYPTED PASSWORD 'mydbpass'"
次にmydb
という名前のデータベースを作成します。
$ sudo -u postgres -- createdb mydb
mydbuser
がmydb
データベースにアクセスできるようにするのも忘れちゃいけませんね。
$ sudo -u postgres -- psql --command "GRANT ALL PRIVILEGES ON DATABASE mydb TO mydbuser"
ここで、PostgreSQLの再起動をしておいた方が無難でしょう。
+$ sudo systemctl restart postgresql
これで、実際にmydb
データベースに、mydbuser
としてログインすることができるようになったはずです。
$ psql -U mydbuser -d mydb -h 127.0.0.1
では、PostgreSQLのセットアップが無事終了したところで、次のコマンドでアプリケーションを立ち上げてみましょう。
+$ stack exec -- servant-on-heroku-api
+running servant-on-heroku on port 8080...
無事に立ち上がったら、コメントを送ってみます。 +アプリが立ち上がった状態で、別のターミナルなどを開いて次のコマンドを打ってみてください。
+$ curl --request POST \
+--header 'Content-Type: application/json' \
+ --data '{"author": "DG", "text": "Pretty good"}' \
+ 'http://localhost:8080/add-comment'
+ { "text": "Pretty good", "author": "DG" }
よさそうですね!
+では、全コメントを取得してみます。
+$ curl --request GET \
+--header 'Content-Type: application/json' \
+ 'http://localhost:8080/get-comments'
+ [{"text":"Pretty good","author":"DG"} ]
いいですね! DG (Dennis Gosnell / 原著者)さんが「チョベリグ!」と言っています。 +以上で、ローカル環境でアプリを動かすことができたので、次はDockerを使ってみましょう!
+Dockerはコンテナ技術を用いたプログラムで、これを使うと仮想環境下でアプリをビルドしたり実際に動かしたりすることができます。 +以降では、読者のみなさまがある程度Dockerについて知っている前提で進めていきますが、たぶんそんなによく知らなくても「まぁそんなもんなんだろう」と思いながら読んでいただければ差し支えないと思います。 +実際、日本語ローカライズ版を作ってる僕だって、そんなにDockerに詳しいわけではありません。
+Dockerのインストール方法は環境によってまちまちなので、ご自身の環境に合わせて信用できるドキュメントを参照してください。 +Arch Linuxの場合やUbuntuの場合はリンク先を読めばなんとかなると思います。
+Dockerのインストールが終わったら、以下のコマンドを実行してDockerがちゃんと動いているか確認してみてください。
+$ docker info
では、実際にDockerを使ってサンプルアプリをビルドし、そのアプリを動かすためのDockerイメージを作成します。
+アプリケーションをビルドするには、docker build
コマンドを使います。
$ docker build -t servant-on-heroku .
このコマンドを実行すると、実行したディレクトリ内に存在するDockerfile
という名前のファイルにしたがってアプリをビルドしてくれます。
+このDockerfile
には、アプリをビルドするための具体的な手順がすべて記述されており、その手続きにしたがって、まったく別の環境でもDockerさえあればアプリを実行できる「イメージ」を作成できます。
ためしに、このサンプルアプリに含まれるDockerfile
の中身を見てみましょう。
+以下の各処理を実行するようになっています。
apt-get
コマンドを使って、依存パッケージをインストールstack
をインストールstack.yaml
を見て、実際に必要なバージョンのGHCをstack
を使ってインストールする*.cabal
ファイルの記述にしたがって、アプリが使っているHaskellパッケージをインストールするstack
を使って実際にアプリをビルドする前述したdocker build
コマンドを実行してservant-on-heroku
という名前のイメージを作成するには1時間近くかかるので2、その間にご飯を食べたり録画しておいたアニメを2本見れます。
docker build
が終わったら、docker images
でローカル環境に存在する全イメージを一覧表示してみましょう。
$ docker images
+REPOSITORY TAG IMAGE ID CREATED SIZE
+servant-on-heroku latest ff591d372461 30 seconds ago 3.92 GB
+...
さきほど作成したservant-on-heroku
のイメージが作成されているのがわかりますね?
では、servant-on-heroku
のイメージを走らせてみましょう。次のコマンドを実行すれば、Docker内でこのサンプルアプリが動くはずです。
$ docker run --interactive --tty --rm servant-on-heroku
あぁ… またPostgreSQLの例の問題が出てしまったみたいですね…
+servant-on-heroku-api: libpq: failed (could not connect to server: Connection refused
+ Is the server running on host "localhost" (::1) and accepting
+ TCP/IP connections on port 5432?
+could not connect to server: Connection refused
+ Is the server running on host "localhost" (127.0.0.1) and accepting
+ TCP/IP connections on port 5432?
+)
+これはどういうことでしょうか。
+servant-on-heroku
コンテナはDockerコンテナとして動いているため、初期設定では我々のローカル環境が見えず、もちろんローカル環境にセットアップしてlocalhost:5432
で動いているPostgreSQLも見えないのです。
では、ちょっとしたワザを使ってこの問題を解決してみましょう。
+servant-on-heroku
コンテナを動かしている時に、Dockerに我々のローカル環境のネットワークインタフェースを使うように指示することができます。
$ docker run --interactive --tty --rm --network host servant-on-heroku
+running servant-on-heroku on port 8080...
ほら、こうすれば、確かにDockerコンテナからPostgreSQLにアクセスできているようです。
+servant-on-heroku
コンテナが動いている状態で別のシェルを立ち上げて、前の章でやったようにcurl
コマンドでAPIが動いているか確かめてみましょう。
+まずはコメントの投稿です。
$ curl --request POST \
+--header 'Content-Type: application/json' \
+ --data '{"author": "EK", "text": "Not enough CT"}' \
+ 'http://localhost:8080/add-comment'
+ { "text": "Not enough CT", "author": "EK" }
今度はコメントの取得をしてみます。
+$ curl --request GET \
+--header 'Content-Type: application/json' \
+ 'http://localhost:8080/get-comments'
+ [{"text":"Pretty good","author": "DG"},{"text":"Not enough CT","author":"EK"}]
この通り、無事にEK (Edward Kmett / Haskell界のすごい人)さんが「圏論を、圏論をもっとくれぇええええい!」と言っているコメントが追加されました。
+ちなみに、Docker内でシェルを開いて、手動でDockerイメージをいじりながらいろいろ確かめてみるには、次のコメントのようにすればOKです。
+$ docker run --interactive --tty --rm --network host servant-on-heroku /bin/bash
では、Docker上でアプリがちゃんと動いたことを確認したところで、ようやくHerokuの出番です。
+Docker上でビルドと実行ができていさえすれば、Herokuにデプロイするのは難しくありません。 +まず最初にHerokuのアカウントを作りましょう。
+Herokuのアカウント作成ページでアカウントを作成してください。 +もちろん、すでにアカウントを持っているのであればあえて別のアカウントを作りなおす必要はないですよ!
+今回はHerokuの無料枠を使ってアプリをデプロイするので、クレジットカードの登録は必要ないです。 +こわくないですね!
+ここで説明する内容は、ほとんどHerokuの公式ドキュメントを参照しているので、なにかわからないところがあったらそちらをチェックしてみてください。
+HerokuはCLIで操作するためのコマンドを用意してくれているので、これを使って便利にHerokuを使い倒せます。 +ちょうど、AWSのCLIとか、Digital OceanのCLIプログラムと同じような感じです。
+Arch Linux使いの方は、下記のコマンドでHerokuのCLIプログラムをインストールできます。
+$ yaourt -S heroku-toolbelt
このコマンドは、heroku
コマンドのバイナリを直接取得してインストールしてくれます。
他の環境の方は、Herokuの公式ドキュメントをご覧ください。
+CLIプログラムのインストールができたら、コマンドライン上でログインして、権限を必要とする操作ができる状態にしておきましょう。
+$ heroku login
このコマンドを実行すると、ユーザ名とパスワードをたずねられるので、事前に作成しておいたアカウントの情報を入力してください。
+今回のサンプルアプリをHerokuで公開するには、まずHeroku上でアプリケーションの登録をする必要があります。
+以下のコマンドを実行すると、servant-on-heroku
という名前のアプリケーションをHerokuに登録できます。
+必要に応じてservant-on-heroku
の部分を別の名前に変更してアプリケーションを登録してください。
$ heroku apps:create servant-on-heroku
以下のコマンドで、いま新規登録されたアプリケーションについての情報を一応取得できます。
+$ heroku apps:info servant-on-heroku
+=== servant-on-heroku
+Auto Cert Mgmt: false
+Dynos:
+Git URL: https://git.heroku.com/servant-on-heroku.git
+Owner: me@gmail.com
+Region: us
+Repo Size: 0 B
+Slug Size: 0 B
+Stack: cedar-14
+Web URL: https://servant-on-heroku.herokuapp.com/
Web URL
の項目だけ、あとで使うのでどこかにメモしておいてください。
+他の項目は、いまは特に気にしなくて大丈夫です。
Herokuのコマンドラインプログラムは、プラグインを追加することで、どんどん便利な機能を使えるようにできます。
+今回は、Heroku Container Registryというプラグインを使いましょう。
+以下のコマンドで、このプラグインがインストールされます。
+$ heroku plugins:install heroku-container-registry
インストールが終わったら、次のコマンドを実行して、ちゃんと動いているか確認してください。
+$ heroku container
+4.1.1
きっと、このプラグインのバージョンナンバーが表示されたはずです。
+実際にプラグインを使うためには、以下のコマンドでHeroku Container Registryにログインする必要があります。
+$ heroku container:login
このコマンドによって、Container Registryのログイン情報が、~/.docker/config.json
というファイルに追加されます。
$ cat ~/.docker/config.json
+{
+"auths": {
+ "registry.heroku.com": {
+ "auth": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx="
+ }
+ }
+ }
実際にアプリをHeroku上で動かすには、以下のコマンドを使います。
+$ heroku container:push web
これを実行すると、実行したディレクトリ内にあるDockerfile
の設定にしたがってDockerイメージを作成します。
+内部では、ローカル環境でDocker
イメージを作成するときに使ったのと同じdocker build
を呼んでいます。
+前の章で実際にdocker build
を実行した方は、その際に作成したイメージがそのままDocker Container Registryに送られるので安心してください。また1時間も待つなんてイヤですよね。
では、heroku apps:info
をもう一度実行して確認してみましょう。
$ heroku apps:info servant-on-heroku
+=== servant-on-heroku
+Auto Cert Mgmt: false
+Dynos:
+Git URL: https://git.heroku.com/servant-on-heroku.git
+Owner: me@gmail.com
+Region: us
+Repo Size: 0 B
+Slug Size: 0 B
+Stack: cedar-14
+Web URL: https://servant-on-heroku.herokuapp.com/
あれ? なにかおかしいですね… Dynos:
のところになにも書いてありません。
+dyno
というのはHerokuが独自に使っている用語で、ウェブアプリを実行する1台のサーバのことを意味します。ここになにも書かれていないということは、アプリを実行しているサーバがいないということになります。
これをどうにかするためには、heroku ps:scale
を使います。
$ heroku ps:scale web=1
これで、“web” dynoが1台分作成され、その上で今回のサンプルアプリが動くようになります。3
+では、次のコマンドを実行して、dynoがちゃんと動いていることを確認しましょう。
+$ heroku ps
+Free dyno hours quota remaining this month: 549h 2m (99%)
+For more information on dyno sleeping and how to upgrade, see:
+https://devcenter.heroku.com/articles/dyno-sleeping
+
+=== web (Free): /bin/sh -c /opt/servant-on-heroku/bin/servant-on-heroku-api (1)
+web.1: starting 2017/03/22 19:05:04 +0900 (~ 8s ago)
なんだか余計な情報もだらだら出てきますが、web dynoが1台分動いていることが確認できます。
+これで、サンプルアプリが動くようになったので、curl
を使って、Web URL
にアクセスしてみましょう。
+(サンプルアプリのWeb URL
は、heroku apps:info
に書いてありましたよね?)
$ curl --request POST \
+--header 'Content-Type: application/json' \
+ --data '{"author": "MS", "text": "Gotta make it professional"}' \
+ 'https://servant-on-heroku.herokuapp.com/add-comment'
なにかおかしいですね… なにもレスポンスが返ってきません。 +なにかエラーが出ているはずなので、Heroku上で起こったエラーを実際に見てみたいです。
+Herokuには、とってもすばらしいログ機能があり、アプリの標準エラーや標準出力を簡単にチェックできます。
+$ heroku logs
+2017-03-22T10:05:49 heroku[web.1]: proc start `/opt/servant-on-heroku/bin/servant-on-heroku-api`
+2017-03-22T10:05:52 app[web.1]: servant-on-heroku-api: libpq: failed (could not connect to server: Connection refused
+2017-03-22T10:05:52 app[web.1]: Is the server running on host "localhost" (127.0.0.1) and accepting
+2017-03-22T10:05:52 app[web.1]: TCP/IP connections on port 5432?
+2017-03-22T10:05:52 app[web.1]: )
+2017-03-22T10:05:52 heroku[web.1]: State changed from starting to crashed
とても便利ですね! +どうやらこれまで何度も見てきた例のエラーがまた出ているようです…
+今回は、Heroku上で動いているPostgreSQLデータベースをちゃんとセットアップしていないのが理由です。
+HerokuはPostgreSQLについてしっかりサポートしてくれている上に、なんと無料枠まで設けてくれています。
+以下のコマンドを実行すれば、PostgreSQLのアドオンが使えるようになります。
+$ heroku addons:create heroku-postgresql:hobby-dev
これで、heroku-postgresql
アドオンを、無料で使えるhobby-dev
利用枠で使えるようになりました。
では、本当にPostgreSQLが作成されたか、以下のコマンドを使って確認してみましょう。
+$ heroku addons:info heroku-postgresql
+=== postgresql-tetrahedral-44549
+Attachments: servant-on-heroku::DATABASE
+Installed at: Wed Mar 22 2017 19:22:14 GMT+0900 (JST)
+Owning app: servant-on-heroku
+Plan: heroku-postgresql:hobby-dev
+Price: free
+State: created
データベースの詳細情報については、pg:info
コマンドを使って見れます。
$ heroku pg:info
+=== DATABASE_URL
+Plan: Hobby-dev
+Status: Available
+Connections: 0/20
+PG Version: 9.6.1
+Created: 2017-03-22 10:22 UTC
+Data Size: 7.2 MB
+Tables: 1
+Rows: 0/10000 (In compliance)
+Fork/Follow: Unsupported
+Rollback: Unsupported
+Add-on: postgresql-tetrahedral-44549
これでPostgreSQLのデータベースが動くようになったので、アプリを再起動しましょう。
+$ heroku ps:restart
もう一度ログを見て、本当にこれでエラーが出なくなったか確かめてみます。
+$ heroku logs
+2017-03-22T10:22:15 heroku[web.1]: State changed from crashed to starting
+2017-03-22T10:22:54 heroku[web.1]: proc start `/opt/servant-on-heroku/bin/servant-on-heroku-api`
+2017-03-22T10:22:56 app[web.1]: Migrating: CREATe TABLE "comment"("id" SERIAL8 PRIMARY KEY UNIQUE,"author" VARCHAR NOT NULL,"text" VARCHAR NOT NULL)
+2017-03-22T10:22:57 heroku[web.1]: State changed from starting to up
すごーい!ついに、ついにちゃんと動いたみたいです!!
+もう一度curl
コマンドを使ってAPIがちゃんと動いているか確認してみます。
$ curl --request POST \
+--header 'Content-Type: application/json' \
+ --data '{"author": "SPJ", "text": "Avoid heroku-at-all-costs"}' \
+ 'https://servant-on-heroku.herokuapp.com/add-comment'
+ {"text":"Avoid heroku-at-all-costs","author":"SPJ"}
ちゃんとレスポンスが返ってきています! +今度はコメントを取得してみましょう。
+$ curl --request GET \
+--header 'Content-Type: application/json' \
+ 'https://servant-on-heroku.herokuapp.com/get-comments'
+ [{"text":"Avoid heroku-at-all-costs","author":"SPJ"}]
いいですね! +SPJ(Simon Peyton Jones / Haskellの父)さんが「目先の便利さにとらわれてHerokuに余計な機能をいれるのは、ダメ。ゼッタイ。」4と言っています。 +これで全てうまくいったようです。
+賢明な読者のみなさんは、「Heroku上のアプリはどうやってデータベースを見つけているんだろう?」と疑問に思ったかもしれません。 +実は、Herokuにはアプリに環境変数を与える仕組みがあります。
+この環境変数の設定値を確かめるには、以下のコマンドが使えます。
+$ heroku config
+=== servant-on-heroku Config Vars
+DATABASE_URL: postgres://someusername:somepassword@ec2-12-12-234-123.compute-1.amazonaws.com:5432/databasename
heroku-postgresql
アドオンでPostgreSQLのデータベースを作成した際に、DATABASE_URL
という名前の設定値が追加されます。
+Herokuはアプリの起動時にこの設定値を環境変数として与えているのです。
+先に述べたとおり、今回のサンプルアプリは、DATABASE_URL
という環境変数を接続先DBの情報として受け取るようになっています5。
Herokuに設定されている環境変数は、heroku config:get VAR_NAME
で取得できるので、次のコマンドを使ってDBに接続することもできます。
$ psql "$(heroku config:get DATABASE_URL)"
+psql (9.6.1)
+SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
+
+databasename=> select * from comment;
+id | author | text
+ ----|--------|---------------------------
+1 | SPJ | Avoid Heroku at all costs
+ (1 row)
Heroku上で動いているアプリをアップデートするのは、とっても簡単です。 +単に以下のコマンドを実行するだけで大丈夫です。
+$ heroku container:push web
このコマンドは、DockerイメージをビルドしなおしてHeroku container registryにアップします。 +その後関係するdynoを全て再起動して、アップデート後のアプリを実行するようにします。
+このサンプルアプリは、いまのままでもいい感じですが、いくつかまだ改善の余地があります。
+一番手っ取り早い改善箇所は、Dockerfile
でしょう。
+Dockerfile
をもっと良くするためのアイディアをいくつか挙げてみます。
Dockerfile
のベースイメージに、もっとファイルサイズが小さいものを使う
現状では、Herokuのイメージを使っていますが、 +たぶんもっと軽いAlpine Linuxを使っても問題はないと思います。
stack
やGHC、その他よく使うHaskellライブラリが最初から入っているイメージをベースにする
こうすることで、一番最初のdocker build
に要する時間をガッツリ削ることができます。
Dockerfile
の一番最後で、stack
やGHC、全Haskellライブラリを削除するようにする
こうすることで、Dockerイメージのサイズを少し減らせる可能性があります。 +Heroku container registryにイメージをアップロードするのが、いくらか早くなるでしょう。
また、docker-compose
などを使って、ローカルで実行する際にもDockerを使ってPostgreSQL DBをセットアップするのも良いかもしれません。
ローカル環境上でDockerが動いていれば、Heroku上でHaskellのコードを動かすのはとても簡単です。 +Herokuの無料枠はアプリのプロトタイプを他の人に試してもらったりするのに最適です。 +もちろん、そのままリリースしたら負荷にたえられないかもしれませんが、アプリ開発の最初期段階にコンセプトを検証したりするのには十分でしょう。
+もし、検証の結果うまくいきそうだと分かったら、クレジットカードを登録して、 +もっと負荷にたえられる有料利用枠でアプリを動かすようにするのだって簡単です。
+僕が自分で許可して、原著者に日本語の内容をチェックしてもらいました。また、翻訳ではなくローカライズなので、原文の逐語訳ではなく、日本語話者にとって理解しやすいように一部加筆修正してあります。↩︎
ここで挙げた7ステップはちょっと複雑です。
+もちろん、1コマンドだけでGHCのインストールから依存ライブラリのインストール、
+アプリ自体のビルドまで完了することもできますが、ここではDockerのキャッシュ機構を活用するために
+いくつものコマンドに分けて記述してあります。
+Dockerのキャッシュ機構によって、docker build
を再実行するときには、入力値が変わったコマンドだけが実行されるようになっています。
+たとえば、servant-on-heroku.cabal
のファイルを変更してdocker build
を再実行すると、
+.cabal
ファイルに書かれた依存ライブラリをインストールする(4)のステップからイメージを再ビルドし始めます。
+キャッシュされているデータを利用するので、(1)から(3)までのステップを省略できるのです。
同じように、src
下のファイルだけを変更してdocker build
を再実行すると、
+(5)のステップ以降のみが実行されます。
+GHCや依存ライブラリをわざわざ再インストールする必要はないからです。
このように、ステップをいくつかに分割することで、ビルド時間を大きく節約することができ、 +2回目以降のビルドが数分で終わるようになります。最初のビルドは1時間もかかっていたのに、ちょろいですね。↩︎
dynoにはいろいろな種類のものがありますが、 +今回のような単純なWeb APIであれば別にこだわる必要はないです。↩︎
元ネタは同氏の“Avoid success at all costs”という言葉で、「目先の便利さにとらわれて、Haskellに余計な機能をいれるのは、ダメ。ゼッタイ。」みたいな意味。↩︎
Herokuは、他にもPORT
という環境変数も使っていて、アプリケーションにどのポートでリクエストを待ち受けるかを指定できます。↩︎
Haskellは学習コストが高いと言われることがよくあります。その理由の一つにWeb上でHaskellの情報を探しても情報源になかなかたどり着けないと云う問題があると思います。日本語の情報となるとなおさら難しくなるでしょう。
+Haskell Antennaはその問題の解決に一石を投じるために作ったサービスです。Haskellに関する最新情報はQiitaやスタックオーバーフローといったCGMやHaskellの情報を中心に発信している個人ブログから見ることが出来ます。Antennaはそういった情報源を一箇所に並べることで、Haskellユーザーが日本語で発信している最新情報に、簡単にアクセスできるようにします。今はAPIをHerokuで運用しているため起動に時間がかかることがありますのでご容赦下さい。
+Antennaでは配信する情報源(RSS)をいつでも募集しています。もし追加すべき情報源にアイデアがあればGitHubレポジトリのREADMEにかかれている方法を参考にPull Requestを送っていただくことが可能ですしSlackの#antennaを通じて提案を行ってもらうことも大歓迎です。Haskellの情報を中心に発信しているブログを持っている方は是非 追加提案をしていただけると助かります。
+すでにSlackチームに所属している皆さんにはお伝えしたとおりですが、Haskell-jpのSlackチームに、SlackArchiveというサービスを導入しました!
+こちらからアーカイブを閲覧できます!
こちらのTechCrunchの記事のとおりですが、Slackでの発言を(Slackの無料枠の1万件を超えて)保存し、Slackチームに所属していない人が閲覧したり、Googleなどで検索したりできるようにしてくれるサービスです。
+我々のように、ベターメーリングリストとしてSlackチームを活用しているチームにぴったりですね!
残念ながら現状SlackArchiveの無料プランを利用しているので、SlackArchive使用開始後の発言のみですが、今後は我々の情報共有が、貴重な資産としてネットの海に残されるようになるはずです。
+今後もネチケットを守って楽しいHaskell-jpライフを! 🙇
みなさん、RedditというWebサービスをご存知でしょうか?
+Wikipedia曰くRedditは、世界のWebサイト全体で9番目に多い利用者数を誇る、巨大フォーラムサービスです。WebサイトのURLや様々な話題をスレッドとして投稿し、コメントを付けたり評価したりすることで、毎日盛んな議論が行われています。
詳しい使い方などについては「Redditの歩き方」というウェブサイトが参考になります。
+今回、我々Haskell-jpでは、dfordivamさんの提案を受け、Reddit上でHaskellについて日本語で投稿したりコメントできるスペース、r/haskell_jpを作成いたしました!
+今後はSlackよりもこちらに、Haskellに関するお悩み相談や、見つけた(パッケージ|ブログ記事|アプリケーション)を共有していただけると幸いです。
+SlackArchiveのこの辺りからさかのぼって、dfordivamさんという方の発言を探していただけるとわかるのですが、RedditはSlackよりもオープンで、人気が高く、より議論に向いたプラットフォームです。
+当然SlackArchiveよりGoogle検索にも引っかかりやすいので、質問やお悩み相談とその回答、見つけたWebサイトの共有などは、Redditを利用したほうがアクセスしてもらいやすいだろう、と考えたためです。
さて、これまでHaskell-jpのSlackではquestionsチャンネルを中心に、Haskellに関する数々の質問や相談が、短い期間ながら行われてきました。randomチャンネルなどでのウェブサイトの共有もたくさんありました。 +今後は、Haskell-jpのsubredditでお悩み相談や議論を行うとして、Slackチームはどうなってしまうのでしょうか?
+ざっくり結論から申しますと、「好きな方を使え」というのが公式の見解です。
+Slackはもともとの日本における人気の高さも手伝い、Haskell-jpでもすでに十分に定着しました。Redditにはないemojiによるreaction機能などもあるので、こちらのほうがよい、と言う人も数多くいるでしょう。
+もちろん、カジュアルな話題(例えば「自分をHaskellの型に例えるなら」とか)ではSlackのほうが敷居が低くてよいでしょう。
まとめると、以下のようなガイドラインとなります。
+以上、今後もSlackチーム、r/haskell_jpともどもよろしくお願いします!
+素(単独)のGHCのバイナリをローカルディレクトリにインストールする方法について簡単に紹介します。
+GHCの新リリースが出た場合などに、stack や haskell-platform 経由ではなく、手軽にインストールして遊べます。
+以下、Linux系での手順です。 ユーザー権限で行えます。
GHC単独品の配布ディレクトリ( https://www.haskell.org/ghc/ )から、目的のバージョンのGHCを選びます。
+例えば、Linux(Debian, Ubuntu, …)用の ghc 8.2.1 のバイナリであれば、以下から、ghc-8.2.1-x86_64-deb8-linux.tar.xz
を選びます。
https://www.haskell.org/ghc/download_ghc_8_2_1.html#linux_x86_64
+$ tar -xvJf ghc-8.2.1-x86_64-deb8-linux.tar.xz
$ mkdir ghc821
tarを展開したトップディレクトリにcdで移動します。
+さらに、--prefix
オプションで、GHCのインストール先のディレクトリを指定します。
$ cd ghc-8.2.1
+$ ./configure --prefix=/home/my/ghc821
tarを展開したトップディレクトリで、makeコマンドを実行します。
+$ make install
これで、--prefix
オプションで指定したディレクトリの bin/
下に、ghc
, ghci
, runghc
などがインストールされます。
+なお、インストール後は、tarで展開した側のディレクトリ以下は削除して構いません。
bin/
ディレクトリ下の、ghc
, ghci
, runghc
などで遊べます。 例えば、以下のようにしてghciを起動できます。
$ ghc821/bin/ghci
+GHCi, version 8.2.1: http://www.haskell.org/ghc/ :? for help
+Prelude> 1+2
+3
+ghc8.2.1のカラフルなエラーメッセージや、compact-regionなど、新機能を手軽に楽しめます、enjoy! :)
+もちろん、環境変数PATHにインストール先のディレクトリを追加しておけば、デフォルトでこのGHCを使用できます。
+本格的にGHCでprojectを作る場合には、素のGHCでなく、stack や haskell-platform をインストールするのが良いでしょう。
+ +環境によっては、libgmpが無いというエラーが発生する場合があります。
+その場合は、以下の対処方法があります。
# apt-get install libgmp10
+# cd /usr/lib/x86_64-linux-gnu
+# ln -s libgmp.so.10 libgmp.so
+ 最近はあまり話題になってないかもしれませんが、 Typed holes
機能についての紹介です。
ghc7.8くらいの頃に導入された機能で、ソースコードの式の中に _
か _
で始まる識別名を書くと、その部分の型を推論してくれます。
例えば、
+= print $ 1.0 + _ main
のようなコードを書いて、 runghc
やghci
やghc
で実行すると、 _
部の型をエラーメッセージで教えてくれます。
例えば、以下のようにエラーメッセージが表示されます。 以下の例では、_
部が、Double
型であることを示しています。
$ runghc test1.hs
+
+.hs:3:22: error:
+ test1Found hole: _ :: Double
+ • In the second argument of ‘(+)’, namely ‘_’
+ • In the second argument of ‘($)’, namely ‘1.0 + _’
+ In the expression: print $ 1.0 + _
+ Relevant bindings include main :: IO () (bound at test1.hs:3:1)
+ • |
+ 3 | main = print $ 1.0 + _
+| ^
_
の替りに、 _hoge
のように名前付けしても構いません。
ghci で、 :t
で型を調べるのと同じように気軽に使えます。 (最近はIDEがサポートしていて、明示的には使わなかったりするかもしれません。)
詳細情報は以下にあります。
+ +Haskell用コンパイラであるGHCには、以下のように4種類の実行方法があります。
+ghc
runghc
(or runhaskell
)ghci
ghc -e
以下、それぞれについて簡単に紹介します。
+なお、本記事では、stack経由ではなく、素のGHCを使う場合について説明しています。
Haskellのソースファイルから、実行ファイルを生成(コンパイル)する方法です。生成された実行ファイルは、ユーザーが明示的に起動することにより実行されます。
+例えばソースファイルが以下の場合に、
+$ cat prog.hs
+
+main = print $ 1 + 2
+以下の様に、ghc
コマンドを使ってコンパイルを実行できます。
$ ghc prog.hs
+[1 of 1] Compiling Main ( prog.hs, prog.o )
+Linking prog ...
+これにより、実行ファイル(prog)と中間ファイル(prog.hi, prog.o)が生成されます。
+$ ls
+prog prog.hi prog.hs prog.o
+生成された実行ファイル(prog)は、ユーザーが明示的に指定して起動します。
+$ ./prog
+3
+ghc
のコンパイル方法についての詳しい説明は、こちらやこちらにあります。
+なお、stackを使用する場合は、ghc
コマンドではなく、stack ghc
コマンドによりコンパイルを実行できます。
Haskellのソースファイルから、実行ファイルを一時的に生成(コンパイル)し、その実行ファイルを起動する方法です。つまり、スクリプト的な実行方法です。
+例えば以下の様に、runghc
コマンド(または別名であるrunhaskell
コマンド)により実行できます。
$ runghc prog.hs
+3
+runghc
の使用方法についての詳しい説明は、こちらにあります。
+なお、stackを使用する場合は、runghc
コマンドではなく、stack runghc
コマンドにより実行できます。
Haskellのソースを対話的に入力する実行方法です。いわゆる、REPL(read-eval-print-loop)と呼ばれるものです。
+例えば以下の様に、ghci
コマンドにより実行できます。
$ ghci
+GHCi, version 8.2.1: http://www.haskell.org/ghc/ :? for help
+
+Prelude> 1+2
+3
+
+Prelude> :q
+Leaving GHCi.
+ghci
の使用方法についての詳しい説明は、こちらにあります。
+なお、stackを使用する場合は、ghci
コマンドではなく、stack ghci
コマンドにより実行できます。
コマンドライン上で、Haskellの「式」を直接入力する実行方法です。1つの式だけを指定できます。
+例えば以下の様に、-e
オプション付きのghc -e
コマンドにより実行できます。
$ ghc -e "1+2"
+3
+ghc -e
の使用方法についての説明は、こちらにあります。
+なお、stackを使用する場合は、ghc -e
コマンドではなく、stack ghc -- -e
コマンドにより実行できます。
以下は、ghc -e
による実行例です。
+少し特殊かもしれない記法も記載していますので、以下は気軽に読み飛ばしてください。
例えば、このようにして数列を手軽に生成できます。
+$ ghc -e '[1..10]'
+[1,2,3,4,5,6,7,8,9,10]
+リスト内包表記も使えます。
+$ ghc -e '[x^2 | x <- [1..10]]'
+[1,4,9,16,25,36,49,64,81,100]
+1行毎に出力することもできます。
+$ ghc -e 'mapM_ print [1..3]'
+1
+2
+3
+入力系の関数も使えます。
+$ ghc -e 'getLine'
+ABC
+"ABC"
+入力だけでなく出力系の関数も使えます。
+$ ghc -e 'getLine >>= putStrLn'
+ABC
+ABC
+do記法も使えます。
+$ ghc -e 'do {x <- getLine; putStrLn x}'
+ABC
+ABC
+入出力のフィルタコマンドも表現できます。
+$ ghc -e 'interact $ unlines . map ("hello " ++) . lines'
+John
+hello John
+Mike
+hello Mike
+ペア形式のデータも簡単に生成できます。
+$ ghc -e "zip [1..3] ['A'..]"
+[(1,'A'),(2,'B'),(3,'C')]
+組み合わせのデータも簡単に生成できます。
+$ ghc -e "(,) <$> [1..3] <*> ['A'..'C']"
+[(1,'A'),(1,'B'),(1,'C'),(2,'A'),(2,'B'),(2,'C'),(3,'A'),(3,'B'),(3,'C')]
+組み合わせを連結したデータでも生成できます。
+$ ghc -e '(++) <$> ["R", "G", "B"] <*> map show [0..3]'
+["R0","R1","R2","R3","G0","G1","G2","G3","B0","B1","B2","B3"]
+ちょっと込み入ったデータも生成できます。
+$ ghc -e '(\x y -> x ++ "-" ++ y) <$> ["2016", "2017", "2018"] <*> ["Jan", "Feb", "Mar"]'
+["2016-Jan","2016-Feb","2016-Mar","2017-Jan","2017-Feb","2017-Mar","2018-Jan","2018-Feb","2018-Mar"]
+実は ghc -eコマンドでは、ghciのコマンドも使えます。 ですので、型を調べる:t
(:type)コマンドを使って型の情報を表示させることもできます。
$ ghc -e ':t foldl'
+foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
+GHC8.2以降であれば、上の例のような一般化された型での表示ではなく、デフォルトの型を考慮してシンプルに表示する:t +d
コマンドも使えます。(詳細はこちら)
$ ghc -e ':t +d foldl'
+foldl :: (b -> a -> b) -> b -> [a] -> b
+名前の情報を表示する:i
(:info)コマンドも使えます。
$ ghc -e ':i foldl'
+class Foldable (t :: * -> *) where
+ ...
+ foldl :: (b -> a -> b) -> b -> t a -> b
+ ...
+ -- Defined in ‘Data.Foldable’
+カインド(種)を表示する:k
(:kind)コマンドも使えます。
$ ghc -e ':k Maybe'
+Maybe :: * -> *
+Bashなどのシェル環境であれば、シェル変数を使った準クォートも使えます!
+$ NUM=5
+$ ghc -e "[1..$NUM]"
+[1,2,3,4,5]
+シェル側から簡単にデータを渡せます。
+$ X1="[1..3]"
+$ X2="['A'..'C']"
+$ ghc -e "zip $X1 $X2"
+[(1,'A'),(2,'B'),(3,'C')]
+interact関数でなく、getContents関数でも入力できます。
+$ cat test.dat # data file for example
+1
+2
+3
+
+$ cat test.dat | ghc -e "lines <$> getContents"
+["1","2","3"]
+入力列に対する累算も容易です。
+$ cat test.dat | ghc -e "(sum . map read .lines) <$> getContents"
+6
+少し記述が長いですが、数値データとして処理させることも出来ます。
+$ cat test.dat | ghc -e 'interact $ unlines . map (show . (*2) . (read::String -> Int)) . lines'
+2
+4
+6
+let式も使えます。
+$ ghc -e "let x = 1; y = 2 in x+y"
+3
+階層指定により特定モジュールの関数を指定することも出来ます。
+$ ghc -e 'Text.Printf.printf "%s %d\n" "abc" 1'
+abc 1
+.ghci
ファイルをカレントディレクトリかホームディレクトリに配置しておけば、実行前に読み込めます。
$ cat .ghci
+import Text.Printf
+
+$ ghc -e 'printf "%s %d\n" "abc" 1'
+abc
+数学関数も使えます。
+$ ghc -e "sin (pi/2)"
+1.0
+数学関数とリスト内包表記の併用も便利です。
+$ ghc -e "[sin (n * pi/8) | n <- [0..4]]"
+[0.0,0.3826834323650898,0.7071067811865475,0.9238795325112867,1.0]
+リスト的な処理は、やはり簡単です。
+$ ghc -e 'replicate 5 "hello"'
+["hello","hello","hello","hello","hello"]
+以上です、enjoy!
+Haskell用コンパイラであるGHCのユーザーズガイド(マニュアル)の在り処について紹介します。 +また、GHCのユーザーズガイドはボリュームが多いため、頻繁に調べる項目を見つけやすいように、いくつかの章や節の在り処を簡単に紹介します。
+GHCの最新版のユーザーズガイド(英語版)は以下にあります。
+ +なお、素晴らしい日本語翻訳版が以下にあります。
+ +GHCのユーザーズガイドはボリュームが多いため、頻繁に調べる項目を見つけやすいように、いくつかの章や節を以下に紹介します。
+ユーザーズガイドのWeb画面右側の”Quick search”の検索ボックスにより、ユーザーズガイド内の検索が可能です。
+参考までに、GHCではなく、Haskell言語仕様の方も以下に紹介します。
+ +Haskellには種(kind)という仕組みがあります。大雑把に言ってしまえば、「型の型」を実現する仕組みです。この仕組みについて、あまり情報が出回っていないようなので、解説記事を残しておこうと思います。
+この記事は、Ladder of Functional Programming (日本語訳)のFIRE LUBLINE(ADVANCED BEGINNER)を対象に、種の仕組みとそれに付随するGHC言語拡張やパッケージを紹介するものです。
+なお、特に断らない限り、対象としてGHC8系を設定しています。stack
を使ってる方はresolver
をLTS Haskell 8以降に設定しておくことを推奨します。
私たちは良きHaskellerなので、トップレベルの関数には以下のように型注釈をつけます:
+increment :: Int -> Int
+= n + 1 increment n
このincrement
という関数は、Int
型の値を受け取って、1を加算したInt
型の値を返します。なので、型システムによってそのような型として検証されます:
1 :: Int) -- ok => (2 :: Int)
+ increment ("str" :: String) -- error!
+ increment (1 :: Double) -- error!
+ increment (1 :: Int) (2 :: Int) -- error! increment (
種も大体同じようなものですが、種は型の形式が正しいかを検証する仕組みです。例えば、以下のデータ型を見てください:
+data Id a = Id a
このデータ型宣言は、
+Id
という名前の型コンストラクタId
という名前の値コンストラクタを作ります1。値コンストラクタId
はa -> Id a
という型をしています2。つまり、値コンストラクタId
は、Id a
という型の値を作れる唯一のコンストラクタになります。そして、値コンストラクタは、何らかの値を受け取らなければId a
を構成できないことが、型システムによって保証できます。
さて、型コンストラクタId
の方はどうでしょうか? データ型宣言からは、型コンストラクタId
はそのままでは型になれず、何らかの型を受け取る必要があるように見えます。ですが、それは誰が保証してくれるのでしょうか? さらには、値コンストラクタは受け取った型a
によって、その型が決まります。例えばもし、a
にMaybe
などの型コンストラクタを入れてしまった場合、値コンストラクタId
の型はMaybe -> Id Maybe
という一見おかしな型になってしまいます。このようにa
にMaybe
を渡すことは実際にはできません。一体どういうメカニズムで、このような一見おかしなものが弾かれるのでしょうか? もう、みなさんお気付きだと思いますが、これを保証する仕組みが種なのです。
値コンストラクタId
が型a -> Id a
という型を持つように、型コンストラクタId
は種* -> *
を持ちます。この種がどういう意味を持つのかを見る前に、まずは種を分析するためのツールを用いて、型の種を見てみましょう。そのツールとは、GHCiのkind
コマンドです。では、使ってみます:
>>> data Id a = Id a
+>>> -- 値コンストラクタIdの型を分析
+>>> :type Id
+Id :: a -> Id a
+>>> -- 型コンストラクタIdの種を分析
+>>> :kind Id
+Id :: * -> *
ここで、type
コマンドは値の名前空間を、kind
は型の名前空間を取っていることに注意してください。:kind 1
というようにkind
コマンドに値を分析させることはできませんし、:type Int
というようにtype
コマンドに型を分析させることはできません。では、このkind
コマンドで、他の幾つかの型の種もみてみます:
>>> :kind Int
+Int :: *
+>>> :kind Maybe
+Maybe :: * -> *
+>>> :kind Either
+Either :: * -> * -> *
+>>> :kind Either Int
+Either Int :: * -> *
なんとなく、種の意味が分かってきましたか? 基本的には、*
が型を、* -> *
は型をとって型を返す型コンストラクタを、* -> * -> *
は型を二つとって型を返す型コンストラクタを表しているようです。型コンストラクタには部分適用もできるようです。ただ、単純に全てが* -> * -> ...
という形の種になるわけではありません。次のようなデータ型を見てみてください:
>>> data AppInt m = AppInt (m Int)
+>>> :type AppInt
+AppInt :: m Int -> AppInt m
+>>> :kind AppInt
+AppInt :: (* -> *) -> *
種に()
が付きました。この型コンストラクタAppInt
は、単純に型をとるようなものではなく、型を一つとる型コンストラクタによって、型が作られます。実際に、値は以下のように作れます:
>>> :type AppInt $ Just 1
+AppInt $ Just 1 :: AppInt Maybe
+>>> :type AppInt [1, 2]
+AppInt [1, 2] :: AppInt []
+>>> :type AppInt $ Right 1
+AppInt $ Right 1 :: AppInt (Either a)
+>>> :type AppInt $ Left True
+AppInt $ Left "str" :: AppInt (Either Bool)
ちょっと不思議な型ですね。型コンストラクタAppInt
は* -> *
にマッチする型コンストラクタしか受け取れません。試してみましょう:
>>> :kind AppInt Int
+
+<interactive>:1:3: error:
+Expected kind ‘* -> *’, but ‘Int’ has kind ‘*’
+ • In the first argument of ‘AppInt’, namely ‘Int’
+ • In the type ‘AppInt Int’
+ >>> :kind AppInt Either
+
+<interactive>:1:3: error:
+Expecting one more argument to ‘Either’
+ • Expected kind ‘* -> *’, but ‘Either’ has kind ‘* -> * -> *’
+ In the first argument of ‘AppInt’, namely ‘Either’
+ • In the type ‘AppInt Either’
エラー文がそのままですね。
+AppInt Int
の方は「* -> *
を期待していたが、受け取った型Int
の種は*
ですよ」と言っています。AppInt Either
の方は「* -> *
を期待していたが、受け取った型コンストラクタEither
の種は* -> * -> *
ですよ」と言っています。このようにして、型注釈によって受け取る値を制限できるように、種によって受け取る型を制限できるわけです。何となく、種がどういうものかは分かっていただけたでしょうか? では、種がどのような意味を持っているのかを、ちゃんと見ていきましょう。
+Haskellには、標準で二種類の種があります。それは
+*
という種k1
、k2
を何かしらの種とした時、k1 -> k2
という形をした種の二つです。今まで見てきたように、
+*
は、データ型k1 -> k2
は、k1
の種を持つ型を受け取りk2
の種を持つ型を返すような型コンストラクタをそれぞれ表します。値コンストラクタから作ったAppInt [1, 2]
が一つの値であったように、型コンストラクタから作ったAppInt Maybe
なども一つのデータ型です。
k1 -> k2
は右結合で解釈されます。なので、* -> * -> *
は、実際には* -> (* -> *)
と同じです。なので、右に括弧が付く場合は省略が可能ですが、左に付く場合は省略ができません。つまり、(* -> *) -> *
と* -> * -> *
は別物になります。
さて、ここで一つ重要な型コンストラクタを紹介しておきましょう。それは関数型コンストラクタ(->)
です。型コンストラクタが()
で囲まれて、新しい表記方法が出てきたように思えますが、どうか落ち着いてください。通常の関数において(値の世界において)、私たちは中置演算子を()
で囲むことで、通常の関数として扱うことができました。型注釈上でも同じようなことができます。
>>> :type id :: a -> a
+id :: a -> a :: a -> a
+>>> :type id :: (->) a a
+id :: (->) a a :: a -> a
上の二つの型は、表記は違えど同じ型を表しています。実は私たちは、中置がデフォルトの型コンストラクタを自分で作ることもできます。それにはTypeOperators
拡張を使わなければいけませんが。ちょっと作ってみましょう:
>>> :set -XTypeOperators
+>>> data a + b = Coproduct (Either a b)
+>>> -- 型コンストラクタ
+>>> :kind (+)
+(+) :: * -> * -> *
+>>> -- 値コンストラクタ
+>>> :type Coproduct
+Coproduct :: Either a b -> a + b
+>>> :type Coproduct $ Right True
+Coproduct $ Right True :: a + Bool
+>>> :type Coproduct $ Left True
+Coproduct $ Left True :: Bool + a
+>>> :kind (+) Int
++) Int :: * -> *
+ (>>> :kind (+) Int Bool
++) Int Bool :: *
+ (>>> :kind Int + Bool
+Int + Bool :: *
残念ながらセクションは使えませんが、その他は大体中置演算子と同じで、部分適用などもできます。関数型コンストラクタ(->)
も、(+)
と似たようなものです。種を見てみましょう:
>>> :kind (->)
+(->) :: * -> * -> *
+>>> :kind (->) Int
+->) Int :: * -> *
+ (>>> :kind (->) Int Bool
+->) Int Bool :: *
+ (>>> :kind Int -> Bool
+Int -> Bool :: *
追記: GHC 8.2.1では、:kind (->)
の表示結果が、TYPE q -> TYPE r -> *
というものに変更されたようです。この表記に関しては、続編の方で解説します。今は、* -> * -> *
と大体同等のものであると思ってもらって構わないので、以降では(->) :: * -> * -> *
であるとして話を進めていきます。宜しくお願いします。
(->)
は二つの型を取り、データ型を返します。そのデータ型とは関数型です。例えば、Int -> Maybe Bool
(関数表記では(->) Int (Maybe Bool)
)はInt
型の値を受け取りMaybe Bool
型の値を返す関数の型を表しているのでしたね。関数型コンストラクタは二つの引数の種を*
に制限しています。なので、Maybe -> Int
といったような型注釈は書けません。これは、型コンストラクタが値を持たないことに反しません。
さて、次のようなデータ宣言を考えてみましょう:
+data Id a = Id a
+data AppInt m = AppInt (m Int)
一番最初に見たデータ宣言です。
+Id
の種は* -> *
、値コンストラクタId
の型はa -> Id a
AppInt
の種は(* -> *) -> *
、値コンストラクタAppInt
の型はm Int -> AppInt m
になるのでした。このデータ宣言には、特に種に関する情報を書いているわけではありません。Id
とAppInt
の種は、どうやって定まったのでしょうか? 実は、種に関する推論によって、これらの種は決定されるのです。
Haskellでは、型注釈なしの関数は、型推論されて型が決まります。種でも同じように、推論が行われます。(->)
の種は* -> * -> *
であったことを思い出してください。値コンストラクタは関数ですから、そのパラメータは*
という種を持つことになります3。
Id
の方を考えてみると、値コンストラクタからa
型は*
という種であることが分かります。AppInt
の方も同じくInt
が*
という種であることとm Int
が*
であることから、m
は* -> *
と推論されます。このようにして、Id
とAppInt
の種は自動的に決まったわけです。では、推論に頼らず種を指定することはできるのでしょうか? 残念ながら、Haskellの標準システムでは、推論に頼らずデータ型コンストラクタの種を指定することはできません。次のようなデータ宣言を考えてみましょう:
data App f a = App (f a)
+data TaggedData t = TaggedData
App
型コンストラクタのパラメータf
とa
は、それぞれ何かしらの種k
に対してk -> *
、k
という形をしていればいいはずですが、実際には* -> *
、*
という型になります。TaggedData
型コンストラクタのパラメータt
も、どのような種であってもいいはずですが、*
となります。このように、標準のHaskellでは、デフォルトで*
が設定されており、確定しないような種は*
として扱われます。なので、型コンストラクタでタグ付けしたデータ型を作るといったことはできません。
上では、種の意味と種推論について話しました。種推論は、種を推論してくれるわけですが、正しく私たちが思ったことを推論してくれるわけではなく、表現できない型コンストラクタもありました。さて、その他にも推論が失敗するようなケースもあります。以下をみてください:
+data Ill m a = Ill (m a) m
型コンストラクタIll
のパラメータm
とa
の種はどうなるでしょうか? 実は、このような場合につじつまが合う種はありません。もしこのデータ宣言が成り立つなら、値コンストラクタIll
の型はIll :: m a -> m -> Ill m a
になりますが、この場合m
が型コンストラクタなことは明白なので型コンストラクタに紐づく値が存在することになりますし、(->)
の種にも合いません。もう一つ、推論が失敗する面白いケースがあります。以下のデータ宣言を考えてみましょう:
data Inf a b = Inf (a b) (b a)
ここで、a
の種をk0 -> *
とおくと、b
の種はk0
になるわけですが、b
もa
を受け取っているのでやはりk1 -> *
というような形をしているはずです。このように、両方に辻褄が合うような種を探していくと、永遠に同じ操作の繰り返しになり終わりません。このような場合も種の推論は失敗し、コンパイルエラーになります。
また、型コンストラクタに型を渡す場合も、種がちゃんと合うかを確認し、種が合わない場合コンパイルエラーになるのでした。このように、コンパイルする際は、種の推論や種の検証を行い、辻褄が合うかを保障し、種を確定させる必要があります。
+さて、Haskellではもう一つコンパイル時に行われる重要な評価があります。それは、型に関する評価です。Haskellのプログラム中の型を推論し、ちゃんと型の辻褄が合っているかも評価しなければなりません。これらの二つの評価はGHCでは別々に行われます。これは当たり前のように思えるかもしれませんが、種に関して考えるときは常に意識しなければなりません。次のプログラムをみてください:
+-- TestKind.hs
+
+module TestKind where
+
+f :: Maybe -> Int
+= 0
+ f _
+g :: Int -> Bool
+'0' = True
+ g = False g _
これをコンパイルすると以下のエラーが出されます:
+$ stack ghc -- -Wall TestKind.hs
+[1 of 1] Compiling TestKind ( TestKind.hs, TestKind.o )
+
+TestKind.hs:5:6: error:
+• Expecting one more argument to ‘Maybe’
+ Expected a type, but ‘Maybe’ has kind ‘* -> *’
+ • In the type signature:
+ f :: Maybe -> Int
ここでは、「Maybe
は* -> *
という種を持っているが、(->)
が期待している種は*
だ」と言っています。ですが、上のプログラムにはもう一つおかしな点があります。それは関数g
の型注釈です。関数g
の受け取る値はInt
型のはずですが、実際にはChar
型の値を受け取っています。ただし、関数g
の型注釈の種に関しては何の問題もありません。
GHCでは、種と型の検査は別々に行われるという話をしました。実は、さらにこの二つの間には評価順序があります。まず種の検査を行ってから、型の検査が行われるようになっているのです。種の検査に失敗すれば型の検査は行われません。これらは、:type
コマンドや:kind
コマンドにも影響するので注意が必要です。:kind
コマンドは種の評価を行いますが、型の評価は行いません。あまり、:kind
コマンド上で種の検査が通って型の検査が通らないといった場面には遭遇しないかもしれないですが、これは心に留めておくと良いでしょう。
この章では、基本的な種の仕組みを紹介しました。種というのは、標準では二つ存在するのでした。それは、以下のものです:
+*
: データ型を表す種k1 -> k2
: k1
の種を持つ型を受け取り、k2
の種を持つ型を返す、型コンストラクタを表す種また、データ宣言において種は推論され、確定しない場合はデフォルトで*
を用いるのでした。また、種と型の評価はそれぞれ別々に行われ、種の評価の後に型の評価が行われることも学びました。
以降では、Haskell標準の種の仕組みを拡張する、幾つかの重要なGHC拡張について話していきましょう。
+Haskell標準では、データ宣言において、型コンストラクタの種は種推論によって決定するのでした。このため、表現できない型コンストラクタがあることも話しました。これは、不便な場合があります。* -> *
の種を持つ型コンストラクタをタグとした、データ型を表現することができないのはもちろんですが、そもそも複雑なデータ型の場合に注釈としての種が欲しかったり、推論に任せずそもそも型の種を明示的に宣言したい場合があるのです。これは、Haskellにおいてトップレベルの関数の型注釈を行うことが、良い風習とされているのと同じですね。例えば、以下のデータ宣言を考えてみてください:
data Complex a b c = Complex (a (Maybe (b c)))
このような場合に、パッとそれぞれのパラメータの種を考えることは出来るでしょうか? 出来る人もいるかもしれませんが、混乱してしまう人もいるでしょう。もし、次のような注釈があればどうでしょうか?
+data Complex (a :: * -> *) (b :: * -> *) (c :: *) = Complex (a (Maybe (b c)))
これならば、値コンストラクタの型について深く考えなくても、それぞれのパラメータがどういう種を持つ型なのかはすぐに分かるようになりますし、どういう意図で書いたのかが明白です。何の注釈もない場合、b
にはMaybe
を渡せばいいのか、それとも具体的な型を渡せばいいのか少し考える必要がありますが、注釈がある場合には種の読み方が分かっていればすぐ分かります。残念ながら、Haskellの標準でこのような注釈を書くことはできません。そこで、KindSignatures
拡張の出番になります。
KindSignatures
はその名の通り、種の注釈を可能にするGHC拡張です。この拡張により、データ宣言や型シノニムなどでも種注釈が書けるようになります。以下のプログラムをみてください:
{-# LANGUAGE KindSignatures #-}
+
+data App f a = App (f a)
+type FlipApp a (f :: * -> *) = App f a
このプログラムでは、App
の方の種は見た目からすぐ分かります。しかし、FlipApp
の方はどうでしょうか? 上記の例では、すぐそばにApp
のデータ宣言があるから分かりますが、App
とFlipApp
の宣言が別々の場所にあることを想像してみてください。Haskellでは、型コンストラクタ(型関数)にはf
やm
、t
をメタ変数として使う文化があるので、それから推測することは可能ですが、明確に知りたい場合には実装を見にいく必要が出てくる場合もあるでしょう。しかし、きちんと種注釈が書いてあれば、混乱を避けることができます。これが一つの種注釈の魅力です。
また、種注釈を明示することで、推論に頼らず種の制約を書きたい場合もあります。よくあるケースはGADTs
拡張を併用する場合です。GADTs
拡張については、今回は詳しく扱いませんので、GADTs
を知らない人は以下は読み飛ばしてください。
GADTs
との併用では、次のような種注釈を書く場合があります:
data GadtsSample :: * -> * where
+GadtsSample :: a -> GadtsSample a
GADTs
のスタイルは、値コンストラクタの型を明示的に書くため、型コンストラクタのパラメータ名を明記する必要がありません。型コンストラクタはその種が分かればいいですし、値コンストラクタはその型が分かれば問題ないからです。通常のデータ宣言では、型コンストラクタと値コンストラクタの型がごっちゃになっているため、このように種の注釈と型の注釈を完全に分離することは困難です。もちろんGADTs
において、パラメータ名に種注釈をつけていく書き方も許容されています。上の表記は、次の表記と同一です:
data GadtsSample (a :: *) where
+GadtsSample :: a -> GadtsSample a
ただし、GADTs
では型コンストラクタのパラメータ名は特に意味を持たないことに注意してください。値コンストラクタの型注釈は、特に型コンストラクタのパラメータ名に名前を合わせる必要はありません:
data GadtsSample (a :: *) where
+GadtsSample :: b -> GadtsSample b -- aを使わなくてもいい!
このため、一番最初に提示したような、型コンストラクタにはその種注釈を、値コンストラクタにはその型注釈をそれぞれ書くというスタイルを好む人も多くいます。これも、一つのKindSignatures
拡張の魅力と言えるでしょう。なにより重要なことは、GADTs
では値コンストラクタの型を明示しないといけないため、意図しない型コンストラクタへの適用を、誤って型注釈に書いてしまう可能性が、通常のデータ宣言より高くなります。種注釈をつけることで、型コンストラクタの意図している種を明示することにより、意図していなかった型コンストラクタの使用法が、種推論によってすりぬけてしまうことを防ぐことができます。
さて、種注釈を行えるようにするKindSignatures
拡張の他に、もう一つ重要な拡張があります。それが、種多相を行えるようにする拡張です。「基本的な種の仕組み」の章で紹介した、以下のデータ宣言を思い出してください:
data App f a = App (f a)
標準では、型コンストラクタApp
は、(* -> *) -> * -> *
という種になるのでした。しかしながら、f :: k -> *
、a :: k
という形をしていれば、どんな種でもいいはずだという話は覚えていますか? f a :: *
になればいいのですから、わざわざ*
に強めてしまう必要はありません。そこで、デフォルトの*
まで具体化をせずに、抽象的に「何かしらのk
の種において、f :: k -> *
、a :: k
という形をしていれば良い」という情報を残すようにするのが、PolyKinds
拡張、種多相の基本的な考え方です。PolyKinds
拡張を有効にする前とした後でのApp
型コンストラクタの種を見てみましょう:
>>> data App f a = App (f a)
+>>> :kind App
+App :: (* -> *) -> * -> *
+>>> -- PolyKinds拡張の有効化
+>>> :set -XPolyKinds
+>>> data App f a = App (f a)
+>>> :kind App
+App :: (k -> *) -> k -> *
PolyKinds
拡張を有効にした後では、デフォルトで具体化が必要ない部分は、k
という形のまま残っているのが見て取れます! 私たちHaskellerは、多相関数で、具体化された型ではなく任意の型についてマッチするような関数を書くことに慣れています。多相関数の場合、具体化されていない型を型変数と呼ぶのでした。種の場合は種変数といったところでしょう。種多相は、GHCの標準パッケージbase
において、様々なところで用いられています。有名なものとしては、Data.Proxy
にあるProxy
データ型がそうです。その種を見てみましょう:
>>> import Data.Proxy
+>>> :kind Proxy
+Proxy :: k -> *
+>>> :type Proxy
+Proxy :: forall k (t :: k). Proxy t
少しProxy
の値コンストラクタの型注釈が分かりにくいですが、Proxy
値コンストラクタは特に引数を取らずProxy t
という値になります。このように実体(値)を持たない型パラメータを幽霊型と言ったりします。Proxy
型コンストラクタは、どんな種でも良いので何かしらの幽霊型t :: k
をとり、Proxy t
というデータ型に成ります。例えば、型コンストラクタを幽霊型として付属させることも可能です:
>>> :type Proxy :: Proxy Maybe
+Proxy :: Proxy Maybe :: Proxy Maybe
不思議なデータ型ですね。種多相がなくても、Proxy
データ型のような幽霊型をパラメータに持つ型コンストラクタを作ることはできます。しかし、種によってそれぞれ型コンストラクタを用意しなければなりません。今回の例のように、種多相を使えば、一つのデータ宣言によって様々な種の型に対応できるようになるのが、魅力的です。また、PolyKinds
拡張は、一緒にKindSignatures
拡張も有効にします。これらを組み合わせることで、明示的に多相化された種の注釈を書くことも可能です。それは、以下のようになります:
>>> data BiTagged (tag1 :: k) (tag2 :: k) = BiTaggedData
+>>> :kind BiTagged
+BiTagged :: k -> k -> *
+>>> :type BiTaggedData
+BiTaggedData :: forall k (tag2 :: k) (tag1 :: k). BiTagged tag1 tag2
このように種変数を使った種注釈も可能です。これを活用すれば、より強力な型コンストラクタを作ることも可能になるでしょう。
+この章では、種に付随する、二つの重要なGHC拡張を紹介しました。
+KindSignatures
拡張は、種注釈を行えるようにする拡張でした。種注釈によってこれまで表現できなかった型コンストラクタが作れるようになるのはもちろんのこと、分かりやすさや種推論による混乱を避けるための明示的な注釈として、この拡張はとても便利でした。
もう一つのPolyKinds
拡張は、種多相を可能にしてくれる拡張でした。標準では、全ての種は具体化され、曖昧なところは全て標準の種*
によって具体化されます。しかし、この拡張によりデフォルトの動作を、抽象化されたまま型変数として残す動作に切り替えることができるようになります。これによって、それぞれの種に対しての具体的な型コンストラクタを用意する必要も無くなります。また、種注釈を多相的に行うことも可能になるのでした。
今回は、種の基本概念と、種に関連するGHC拡張を紹介しました。
+続編4では、*
の他の幾つかの種と、種とは別の型の分類についての紹介などを踏まえた、幾つかの種に関連する話題について、話したいと思います。
追記: 続編を書きました。続きが気になる方は、読んでみてください。
+KindSignatures
拡張の概要が書かれています。昨日、私山本悠滋はHaskell入門者LT会 - connpassで、ゲストとして参加し、我らがHaskell-jpの宣伝をして参りました。
+発表資料自体はこちらのSlideshareのページに載せたのですが、残念ながら貼り付けているリンクがすべて無効になってしまいました。
+本資料は、Haskell-jpの現在進行中の活動や、今後の目標などを伝えると同時に、そのリンク集としての意味合いが強いので、これでは致命的です。
そこで、本記事ではスライドの内容のうち、Haskell-jpの現在の活動や今後についての箇所をほぼそのまま貼り付けて再共有することにいたします。
+この機会に何かしらの活動に興味を持っていただければ幸いです。
↓以下がその内容となります。 +markdownで書いといてよかった! (⌒_⌒)
+以上が発表内容です。
+いかがでしょうか?気になる活動があればぜひぜひ参加してみてください!
Haskell用コンパイラであるGHCは、驚くほど多彩な情報出力機能を標準で搭載しています。
+出力できる情報は非常に沢山ありますが、ここでは、以下のいくつかの方法について簡単にまとめて紹介します。
なお、本記事では、stackコマンド経由ではなく、素のGHCを使う場合について説明しています。
+stackコマンドを使用する場合は、ghc
コマンドではなく、stack ghc --
コマンドの様に読み替えてください。
+また、本記事の実行例は、GHC8.2.1とLinux(ubuntu 16.04 LTS)環境によるものです。
ここでは、GHCの対話環境(REPL)であるghciコマンドを用いた、情報の出力方法について紹介します。
+:t
コマンド):t +d
コマンド):t +v
コマンド):k
コマンド):i
コマンド)以下、各々の例について説明します。
+:t
コマンド)GHCの対話環境であるghci上で、:t
コマンド(または省略しない形の:type
コマンド)を実行することにより、変数や関数などについての型の情報を表示できます。
+以下は、:t length
コマンドの実行例です。
+ghci
コマンドを起動してから、:t length
コマンドを実行すると、
$ ghci
+Prelude> :t length
次の様に出力されます。
+length :: Foldable t => t a -> Int
上の例では、「length関数は、t a
型の値を入力して、Int
型の値を返す関数である」ことを示しています。 また、「型変数t
はFoldable
クラスに属する」ことを示しています。
:t +d
コマンド)GHC8.2で導入された新しい機能です。
+:t +d
コマンド(または省略しない形の:type +d
コマンド)を実行することにより、デフォルトの型を考慮して、型の情報を分かりやすく出力できます。
+:t +d length
コマンドを実行すると、
Prelude> :t +d length
次の様に出力されます。
+length :: [a] -> Int
上の例では、+d
オプションをつけない:type
コマンドと比べると、Foldable t => t a
の部分がリスト型として具体化されて出力されています。
:t +v
コマンド)GHC8.2で導入された新しい機能です。
+:t +v
コマンド(または省略しない形の:type +v
コマンド)を実行することにより、型についての情報をより詳しく出力できます。
+:t +v length
コマンドを実行すると、
Prelude> :t +v length
次の様に出力されます。
+length :: Foldable t => forall a. t a -> Int
上の例では、+v
オプションをつけない:type
コマンドと比べると、forall a.
の部分が詳しく出力されています。
:k
コマンド):k
コマンド(または省略しない形の:kind
コマンド)を実行することにより、種(カインド)についての情報を出力できます。
+:k Maybe
コマンドを実行すると、
Prelude> :k Maybe
次の様に出力されます。
+Maybe :: * -> *
上の例では、Maybe
型は、*
の種を入力し、*
の種を返す型であることを出力します。
:i
コマンド):i
コマンド(または省略しない形の:info
コマンド)を実行することにより、その名前の定義情報などを出力できます。
+:i length
コマンドを実行すると、
Prelude> :i length
次の様に出力されます。
+class Foldable (t :: * -> *) where
+...
+ length :: t a -> Int
+...
+ -- Defined in ‘Data.Foldable’
上の例では、length
の定義内容(関数であり、Foldableクラスに属しており、Data.Foldableモジュール内で定義されていること)が出力されています。
他にもghciのコマンドにより、様々な情報を対話的に出力させることが出来ます。
+:show imports
: importしているモジュールの一覧出力:help
: コマンドのヘルプghciのコマンドの詳細は、こちらを参照してください。
+ghciのコマンドや、型や種(カインド)については、こちらやこちらも参考になります。
ここでは、GHCのコンパイル時における、情報の出力方法について紹介します。
+以下、各々の例について説明します。
+GHCは、コンパイル時に、中間言語変換などの複数のパイプラインステージを経ながら、最終的にアセンブリコードを生成します。
+コンパイル時に情報出力用のオプションを指定することにより、それらの各ステージごとの中間コードを出力できます。
例えば、以下の単純なソースの場合について例を示します。
+$ cat Func1.hs
+
+module Func1 where
+
+f1 :: Int -> Int -> Int
+= x + y f1 x y
以下、GHCのコンパイル・パイプラインにおける、パーサーやリネームなどの各ステージ後のコード出力例を示します。
+GHCのコンパイルにおける、パーサーのステージ直後のコードを出力するには、-ddump-parsed
オプションを指定します。
$ ghc -ddump-parsed Func1.hs
+パーサー後のコードが、以下の様に出力されます。
+1 of 1] Compiling Func1 ( Func1.hs, Func1.o ) [flags changed]
+ [
+==================== Parser ====================
+module Func1 where
+f1 :: Int -> Int -> Int
+= x + y f1 x y
上の例では、元のソースと同じコードが表示されました。
+以下は、リネームのステージ直後のコードを出力する、-ddump-rn
オプションの例です。
$ ghc -fforce-recomp -ddump-rn Func1.hs
+以下の様に出力されます。
+1 of 1] Compiling Func1 ( Func1.hs, Func1.o )
+ [
+==================== Renamer ====================
+ :: Int -> Int -> Int
+ Func1.f1= x_aRf + y_aRg Func1.f1 x_aRf y_aRg
上の例では、ユニークな識別子に変換されたコードが出力されています。
+なお、上の例では、ソースファイルの最終更新時刻が、オブジェクトファイルよりも古い場合でも強制的に再コンパイルさせる-fforce-recomp
を指定しています。
以下、いくつかのステージ後の中間コードの出力例を、続けて示します。
+$ ghc -O -fforce-recomp -ddump-ds Func1.hs
+:
+ f1 :: Int -> Int -> Int
+LclIdX]
+ [
+ f1= \ (x_aSt :: Int) (y_aSu :: Int) ->
+ + @ Int GHC.Num.$fNumInt x_aSt y_aSu
+ :
上の例では、Haskell言語の構文から、GHCの内部表現の1つであるCore言語に変換されたコードが出力されています。
+Core言語は、非常にシンプルな要素で構成された関数型言語です。
$ ghc -O -fforce-recomp -ddump-stg Func1.hs
+:
+ :: GHC.Types.Int -> GHC.Types.Int -> GHC.Types.Int
+ Func1.f1GblId,
+ [Arity=2,
+ Caf=NoCafRefs,
+ Str=<S(S),1*U(U)><S(S),1*U(U)>m,
+ Unf=OtherCon []] =
+ GHC.Num.$fNumInt_$c+ eta_B2 eta_B1;
+ \r [eta_B2 eta_B1] :
上の例では、さらに、GHCの内部表現の1つであるSTG言語に変換されたコードが出力されています。
+STG言語は、非常にシンプルな要素で構成された、GHCの動作モデルと結びついた関数型言語です。
$ ghc -O -fforce-recomp -ddump-opt-cmm Func1.hs
+:
+ // [R3, R2]
+ Func1.f1_entry()
+ { [(c15y,:
+ Func1.f1_infoconst 8589934607;
+ const 0;
+ const 14;)]
+
+ }
+ {offset: // global
+ c15y// nop
+ // nop
+ GHC.Num.$fNumInt_$c+_info(R3, R2) args: 8, res: 0, upd: 8;
+ call
+ }
+ }:
上の例では、さらに、GHCの内部表現の1つであるCmm言語に変換されたコードが出力されています。
+Cmm(C minus minus)言語は、C言語とアセンブリ言語の中間的な位置づけの、手続き型言語です。
$ ghc -O -fforce-recomp -ddump-asm Func1.hs
+:
+ .globl Func1.f1_info
+.type Func1.f1_info, @object
+:
+ Func1.f1_info:
+ _c15yGHC.Num.$fNumInt_$c+_info
+ jmp :
上の例では、最終的なターゲットCPU用のアセンブリ言語の命令コードが出力されています。
+詳細は省略しますが、他にも様々なステージの情報を出力させることが出来ます。
+-ddump-simpl
: Core中間言語での最適化中コード-ddump-prep
: Core中間言語での最適化最終コード-ddump-cmm
: Cmm中間言語での最適化中コード-ddump-llvm
: LLVM版の命令列中間コードの出力方法の詳細は、こちらを参照してください。
+中間言語については、こちらも参考になります。
コンパイル時に得られる型情報を出力することが出来ます。
+例えば、以下の単純なソースの場合について例を示します。
$ cat Func2.hs
+
+module Func2 where
+
+type Count = Int
+
+f2 :: Count -> Count -> Count
+= a + b f2 a b
コンパイル時に得られる型情報を出力するには、-ddump-types
オプションを指定します。
$ ghc -ddump-types Func2.hs
+以下の様に出力されます。
+1 of 1] Compiling Func2 ( Func2.hs, Func2.o )
+ [TYPE SIGNATURES
+ f2 :: Count -> Count -> Count
+TYPE CONSTRUCTORS
+type Count = Int
+ COERCION AXIOMS
+Dependent modules: []
+Dependent packages: [base-4.10.0.0, ghc-prim-0.5.1.0,
+-gmp-1.0.1.0] integer
それぞれの型の情報が出力されています。
+コンパイル時に得られる正格性についての情報を出力することが出来ます。
+例えば、以下の単純なソースの場合について例を示します。
+少し恣意的な例ですが、関数f3の第1引数は正格(かつ関数内で使用されている)、第2引数は非正格(かつ関数内で使用されている)、第3引数は非正格(かつ関数内で使用されていない)という場合の例です。
$ cat Func3.hs
+
+module Func3 where
+
+f3 :: Int -> Int -> Int -> Int
+0 y z = -1
+ f3 = abs y f3 x y z
コンパイル時に得られる、関数の各引数についての正格性の情報を出力するには、-ddump-str-signatures
オプションを指定します。
+なお、正格性についての最適化を行うために、-O
オプションも併せて指定します。
$ ghc -O -ddump-str-signatures Func3.hs
+正格性についての情報が、以下の様に出力されます。
+1 of 1] Compiling Func3 ( Func3.hs, Func3.o )
+ [
+==================== Strictness signatures ====================
+Func3.$trModule: m
+: <S(S),1*U(1*U)><L,1*U(U)><L,A>m
+ Func3.f3
+
+
+==================== Strictness signatures ====================
+Func3.$trModule: m
+: <S(S),1*U(1*U)><L,1*U(U)><L,A>m Func3.f3
上の例によると、第1引数は、<S(S),1*U(1*U)>
の部分により表されています。 ここでのS(S)
は、引数が正格(Strict)であることを示しています。 1*U(1*U)
は、引数が関数内で使用(Use)されていることを示しています。
+第2引数は、<L,1*U(U)>
の部分により表されています。 L
は引数が非正格(non-strict)であることを示しています。 1*U(U)
は、引数が関数内で使用(Use)されていることを示しています。
+第3引数は、<L,A>
の部分により表されています。 L
は引数が非正格(non-strict)であることを示しています。 A
は、引数が関数内で不使用(Absence)であることを示しています。
正格性解析については、こちらも参考になります。
+他にも、コンパイル時に様々な情報を出力させることが出来ます。
+ +コンパイル時のオプションの詳細は、こちらを参照してください。
+ここでは、GHCによりコンパイルされた実行オブジェクトについて、実行時に情報を出力する方法を紹介します。
+以下、各々の例について説明します。
+GHCによりコンパイルされた実行オブジェクトについて、実行時の統計情報を出力できます。
+例えば、以下のソースの場合について例を示します。
$ cat Prog1.hs
+
+module Main where
+
+main :: IO ()
+= print $ sum [1..10000000] main
まずは、普通にコンパイルを行います。
+ここでは、通常の例を示すために、標準的な最適化を行う-O
オプションを指定しています(プロファイル取得に必須ではありません。)
$ ghc -O Prog1.hs
+実行時の統計情報を出力するには、以下の様に、実行オブジェクトの起動時に+RTS
-s
オプションを指定します。 (+RTS
以降の引数が、GHCのランタイムシステム(RTS)に引き渡されます。 ここでは、-s
が統計情報出力のためのオプションです。)
$ ./Prog1 +RTS -s
+以下の様に、実行時の統計情報が出力されます。
+50000005000000
+ 320,051,552 bytes allocated in the heap
+ 23,320 bytes copied during GC
+ 44,504 bytes maximum residency (2 sample(s))
+ 29,224 bytes maximum slop
+ 2 MB total memory in use (0 MB lost due to fragmentation)
+
+ Tot time (elapsed) Avg pause Max pause
+ Gen 0 304 colls, 0 par 0.004s 0.004s 0.0000s 0.0002s
+ Gen 1 2 colls, 0 par 0.000s 0.000s 0.0000s 0.0000s
+
+ INIT time 0.000s ( 0.000s elapsed)
+ MUT time 0.532s ( 0.549s elapsed)
+ GC time 0.004s ( 0.004s elapsed)
+ EXIT time 0.000s ( 0.000s elapsed)
+ Total time 0.536s ( 0.553s elapsed)
+
+ %GC time 0.7% (0.7% elapsed)
+
+ Alloc rate 601,600,661 bytes per MUT second
+
+ Productivity 99.3% of total user, 99.3% of total elapsed
+上の例では、全体の実行時間やヒープの割当量やGCの概況等が出力されています。
+表示内容の詳細については、こちらを参照してください。
+GHCによりコンパイルされた実行オブジェクトについて、実行時の時間プロファイル情報を出力できます。
+例えば、以下のソースの場合について例を示します。
$ cat Prog2.hs
+
+module Main where
+
+main :: IO ()
+= print f1
+ main
+f1 :: Int
+= sum [1..10000000] + f2
+ f1
+f2 :: Int
+= sum [1..10000000] f2
プロファイルを取るためには、以下の様にまず、コンパイル時に、-rtsopts
-prof
-fprof-auto
オプションを指定します。
+-rtsopts
は、実行オブジェクトの実行時に、GHCのランタイムシステム(RTS)用の引数を使用可能にするオプションです。 -prof
は、プロファイル用のコードを埋め込むためのオプションです。 -fprof-auto
は、プロファイルを取得する対象を自動で設定するオプションです。
$ ghc -O -rtsopts -prof -fprof-auto Prog2.hs
+実行時の時間プロファイルを出力するには、実行オブジェクトの起動時に+RTS
-p
オプションを指定します。 (-p
が時間プロファイル情報出力のためのオプションです。)
$ ./Prog2 +RTS -p
+これにより、時間プロファイル情報が、Prog2.prof
ファイルに以下の様に出力されます。
$ cat Prog2.prof
+ Sun Sep 3 18:01 2017 Time and Allocation Profiling Report (Final)
+
+ Prog2 +RTS -p -RTS
+
+ total time = 0.03 secs (25 ticks @ 1000 us, 1 processor)
+ total alloc = 49,688 bytes (excludes profiling overheads)
+
+COST CENTRE MODULE SRC %time %alloc
+
+f2 Main Prog2.hs:11:1-22 60.0 0.0
+f1 Main Prog2.hs:8:1-27 40.0 0.1
+MAIN MAIN <built-in> 0.0 1.3
+CAF GHC.IO.Handle.FD <entire-module> 0.0 69.8
+CAF GHC.IO.Encoding <entire-module> 0.0 5.6
+CAF GHC.Conc.Signal <entire-module> 0.0 1.3
+main Main Prog2.hs:5:1-15 0.0 21.3
+ :
+上の例では、mainやf1やf2などについて、関数ごとの実行時間が出力されています。
+時間プロファイルの表示内容の詳細については、こちらを参照してください。
+時間プロファイルについては、こちらや、こちらも参考になります。
GHCによりコンパイルされた実行オブジェクトについて、実行時の空間プロファイル情報、つまり、ヒープメモリの使用状況等を出力できます。
+例えば、以下のソースの場合について例を示します。
$ cat Prog3.hs
+
+module Main where
+
+main :: IO ()
+= print $ f1 [1..1000000]
+ main
+= 0
+ f1 [] :xs) = (abs x) + (f1 xs) f1 (x
プロファイルを取るためには、以下の様にまず、コンパイル時に、-rtsopts
-prof
-fprof-auto
オプションを指定します。
+-rtsopts
は、実行オブジェクトの実行時に、GHCのランタイムシステム(RTS)用の引数を使用可能にするオプションです。 -prof
は、プロファイル用のコードを埋め込むためのオプションです。 -fprof-auto
は、プロファイルを取得する対象を自動で設定するオプションです。
ここでは、メモリ使用状況を分かりやすくするために、最適化のレベルを下げる-O0
オプションを指定しています(プロファイル取得に必須ではありません)。
$ ghc -O0 -rtsopts -prof -fprof-auto Prog3.hs
+実行時の空間プロファイルを出力するには、実行オブジェクトの起動時に+RTS
-hc
オプションを指定します。 (-hc
が時間プロファイル情報出力のためのオプションです。)
$ ./Prog3 +RTS -hc -i0.1
+これにより、プロファイル情報が、Prog3.hp
ファイルに生成されます。
なお、上の例では、-i
オプションにより、プロファイルを取得する間隔を秒単位で指定しています。この例では、0.1秒単位にプロファイル情報を取得しています。秒数は、状況に併せて調整してください。
さらに、以下の様に、GHCに標準で付属しているhp2ps
コマンドを実行することにより、生成されたプロファイル情報を、、PostScriptファイルに変換できます。
$ hp2ps -e8in -c Prog3.hp
+さらに、OSプラットフォームに応じたコマンドなどにより、PostScriptファイルをPDFに変換します。 ここでは、ps2pdfコマンドを使用しています。
+$ ps2pdf Prog3.ps > Prog3.pdf
+これにより、以下の様に、空間プロファイルがグラフィカルに表示されます。
+空間プロファイルの表示内容の詳細については、こちらを参照してください。
+空間プロファイルについては、こちらや、こちらも参考になります。
GHCによりコンパイルされた実行オブジェクトについて、実行時のイベント情報を出力できます。
+実行時のイベントとは、GHCのランタイムシステム(RTS)におけるスレッドスケジューラやGCなどの動作情報や、ユーザー指定による動作情報です。
+例えば、以下のソースの場合について例を示します。
$ cat Prog4.hs
+
+module Main where
+
+import Control.Concurrent (forkIO, threadDelay, myThreadId)
+
+main :: IO ()
+= do
+ main
+ forkIO sub
+ forkIO sub2 * 1000 * 1000)
+ threadDelay (
+sub :: IO ()
+= do
+ sub >>= print
+ myThreadId print $ sum [1..100000]
実行時イベントを出力するためには、以下の様にまず、コンパイル時に、-rtsopts
-eventlog
オプションを指定します。-eventlog
が、イベントログを出力するためのオプションです。
$ ghc -O -rtsopts -eventlog Prog4.hs
+さらに、実際に実行時イベントを出力するには、実行オブジェクトの起動時に+RTS
-l
オプションを指定します。 (-l
が実行時イベントを出力するためのオプションです。)
$ ./Prog4 +RTS -l
+これにより、実行時のイベント情報が、Prog4.eventlog
ファイルにバイナリ形式で出力されます。
+バイナリ形式の出力を表示するには2種類の方法があります。
+ThreadscopeコマンドによりGUIで表示する方法と、ghc-eventsコマンドによりテキスト形式で表示する方法です。
+どちらも、コマンドを別途インストールする必要があります。
以下は、1つめの方法であるThreadScopeを用いた表示例です。
+まず、こちらからthreadscope
のバイナリを入手するか、以下の方法により、threadscope
コマンドをインストールしてください。
$ stack install threadScope
+そして、以下のコマンドによりThreadscopeを起動します。
+$ threadscope Prog4.eventlog
+以下の様にGUIが起動し、スレッドの稼動状態やGCの状態を可視化できます。
+上の例では、緑の部分がスレッドの稼働中、橙の部分がGCの稼働中を表しています。 +また、同時に1つスレッドのみが稼働していることが分かります(物理CPUが1個に制限されたハードウェアでの実行例です。)
+以下は、2つめの方法であるghc-eventsを用いた表示例です。
+まず、以下の方法により、ghc-events
コマンドをインストールしてください。
$ stack install ghc-events
+そして、以下のコマンドにより、イベントの内容をテキスト形式で表示できます。
+$ ghc-events show Prog4.eventlog
+以下の様に、テキスト形式でイベント状況が詳細に表示されます。
+Event Types:
+0: Create thread (size 4)
+1: Run thread (size 4)
+2: Stop thread (size 10)
+3: Thread runnable (size 4)
+4: Migrate thread (size 6)
+8: Wakeup thread (size 6)
+9: Starting GC (size 0)
+ :
+Events:
+132911: created capset 0 of type CapsetOsProcess
+134098: created capset 1 of type CapsetClockDomain
+135704: created cap 0
+136473: assigned cap 0 to capset 0
+137241: assigned cap 0 to capset 1
+141152: capset 1: wall clock time 1504529318s 220117000ns (unix epoch)
+142339: capset 0: pid 4626
+144644: capset 0: parent pid 3460
+ :
+250875: cap 0: creating thread 1
+253948: cap 0: running thread 1
+276507: cap 0: creating thread 2
+277764: cap 0: creating thread 3
+281117: cap 0: stopping thread 1 (blocked on threadDelay)
+286774: cap 0: running thread 2
+334686: cap 0: stopping thread 2 (thread yielding)
+337829: cap 0: running thread 3
+341321: cap 0: stopping thread 3 (blocked on an MVar)
+343975: cap 0: running thread 2
+349563: cap 0: waking up thread 3 on cap 0
+3108630: cap 0: stopping thread 2 (heap overflow)
+ :
+上の例では、スレッドの生成や停止の状況が詳細に出力されています。
+実行時イベントログ機能の詳細については、こちらを参照してください。
+実行時イベントログ機能については、こちらも参考になります。
GHCによりコンパイルされた実行オブジェクトについて、実行時のコードカバレッジ情報を出力できます。
+例えば、以下のソースの場合について例を示します。
$ cat Prog5.hs
+
+module Main where
+
+main :: IO ()
+= print f1
+ main
+= f2 3
+ f1
+3 = f3
+ f2 = f4
+ f2 _
+= 30
+ f3 = 40 f4
コードカバレッジを取得するためには、以下の様にまず、コンパイル時に、-fhpc
オプションを指定します。
$ ghc -fhpc Prog5.hs
+さらに、コンパイルされた実行オブジェクトを起動するだけで、実行時のコードカバレッジを出力できます。
+$ ./Prog5
+これにより、コードカバレッジ情報が、Prog5.tix
ファイルにバイナリ形式で出力されています。
さらに、以下の様に、GHCに標準で付属しているhpc report
コマンドを実行することで、コードカバレッジの概況をテキスト形式で出力できます。
$ hpc report Prog5
+以下の様に、コードカバレッジの概況がテキスト形式で出力されます。
+ 75% expressions used (6/8)
+100% boolean coverage (0/0)
+ 100% guards (0/0)
+ 100% 'if' conditions (0/0)
+ 100% qualifiers (0/0)
+ 50% alternatives used (1/2)
+100% local declarations used (0/0)
+ 80% top-level declarations used (4/5)
+また、以下の様に、hpc markup
コマンドを実行することで、HTMLファイルの形式で情報を出力することもできます。
$ hpc markup Prog5
+以下の様に、モジュールごとに詳細なHTMLファイルが生成されます。
+$ ls *html
+Main.hs.html hpc_index_alt.html hpc_index_fun.html
+hpc_index.html hpc_index_exp.html
+生成されたHTMLファイルでは、モジュールごとの概況が以下の様に表示されます。
+さらに、モジュール内の詳細状況も以下の様に表示されます。
+上の例では、f4は、評価(実行)されていないパスである事が分かります。
+コードカバレッジの詳細については、こちらを参照してください。
+コードカバレッジについては、こちらも参考になります。
GHCによりコンパイルされた実行オブジェクトについて、実行時のエラー発生時のスタックトレース情報を出力できます。
+例えば、以下のソースの場合について例を示します。
$ cat Prog6.hs
+
+module Main where
+
+main :: IO ()
+= print f1
+ main
+= f2 + 1
+ f1
+= f3 + 1
+ f2
+= 1 `div` 0 f3
スタックトレースを取るためには、以下の様にまず、コンパイル時に、-rtsopts
-prof
-fprof-auto
オプションを指定します。
+-rtsopts
は、実行オブジェクトの実行時に、GHCのランタイムシステム(RTS)用の引数を使用可能にするオプションです。 -prof
は、プロファイル用のコードを埋め込むためのオプションです。 -fprof-auto
は、プロファイルを取得する対象を自動で設定するオプションです。
$ ghc -rtsopts -prof -fprof-auto Prog6.hs
+実行時におけるエラー時のスタックトレースを出力するには、実行オブジェクトの起動時に+RTS
-xc
オプションを指定します。 (-xc
がスタックトレース出力のためのオプションです。)
$ ./Prog6 +RTS -xc
+これにより、エラー時のスタックトレース情報が、次の様に出力されます。
+*** Exception (reporting due to +RTS -xc): (THUNK_STATIC), stack trace:
+GHC.Real.CAF
+ --> evaluated by: Main.f3,
+ Main.CAF
+ called from --> evaluated by: Main.f2,
+ Main.CAF
+ called from --> evaluated by: Main.f1,
+ Main.CAF
+ called from --> evaluated by: Main.main,
+ Main.CAF
+ called from Prog6: divide by zero
上の例では、エラーはf3で発生しており、f3はf2, f1, mainの順に呼び出されてきたことが分かります。
+スタックトレースの詳細については、こちらを参照してください。
+スタックトレースについては、こちらも参考になります。
その他、実行時のオプションの詳細は、こちらを参照してください。
+さらに、以下の様に、コンパイラ自身についての、情報を出力する方法もあります。
+ghc --version
: GHCのバージョンを出力ghc --info
: GHCのコンパイラ自身の詳細情報を出力ghc --show-options
: GHCのオプション一覧を出力ghc --supported-extensions
: GHCの言語拡張一覧を出力ghc --print-libdir
: GHCが参照するパッケージのディレクトリ一覧を出力ghc-pkg list
: GHCが参照するパッケージの一覧を出力Happy Hacking!
+以上です。
+Haskellには種(kind)という仕組みがあります。大雑把に言ってしまえば、「型の型」を実現する仕組みです。この仕組みについて、あまり情報が出回っていないようなので、解説記事を残しておこうと思います。なお、前編と後編に分かれていて、この記事は後編になります。前編はこちらになります。
+この記事は、Ladder of Functional Programming (日本語訳)の FIRE LUBLINE(ADVANCED BEGINNER) を対象に、Part 1の続きとして、種に付随するGHC言語拡張やパッケージを紹介するものです。
+なお、特に断らない限り、対象としてGHC8系を設定しています。stack
を使ってる方はresolver
をLTS Haskell 7以降に設定しておくことを推奨します。
前回の記事では、種の基本的な仕組みを紹介しました。全てのデータ型は*
という種を持っており、データ宣言は*
の種を持つ型を作る型コンストラクタを定義するのでした:
>>> newtype WrappedInt = WrappedInt Int
+>>> :kind WrappedInt
+WrappedInt :: *
+>>> data Tag a = Tag
+>>> :kind Tag
+Tag :: * -> *
さて、Haskell標準には上のようなデータ型を表す*
と、型コンストラクタを表すk1 -> k2
という形の種(例えば、* -> *
や* -> (* -> *)
など)しかありませんでした。GHCでは、他にもいくつか種を導入しています。今日は、その幾つかを紹介していきます。一つ目が、型制約を表す種Constraint
です。この種を伴う仕組みはConstraintKinds
拡張により導入できます。
Haskellの型上には、データ型や型コンストラクタの他にも、型制約という登場人物がいます。型制約は名前の通り、型の制約が書けるようにするものです。以下の関数をみてください:
+minByOrd :: Ord a => a -> a -> a
+= if x < y then x else y minByOrd x y
この関数minByOrd
は、型a
が順序を持つ(Ord
クラスのインスタンスである)という制約を満たしている時、二つの引数のうち小さい方をOrd
のメソッド<
を使用して返します。
型制約は、型クラスを使うことで作ることができます。例えば、ある型がデフォルトの値を持つという制約は、以下のように書けます:
+class HasDefault a where
+ defaultValue :: a
この型クラスを使うことで、デフォルトの値を持つ型制約を満たしている型上では、defaultValue
メソッドを使用することができるようになります。例えば、以下のようにです:
fromMaybe :: HasDefault a => Maybe a -> a
+Just x) = x
+ fromMaybe (Nothing = defaultValue fromMaybe
この関数は、型a
がHasDefault a
という型制約を満たしているならば、Maybe a
の値をパターンマッチし、中身がJust
ならそのままJust
を外して値を返し、Nothing
ならデフォルト値をdefaultValue
メソッドを使用して、返します。
ここからが本題です。実はGHC上では、型制約にも種が割り当てられています。見てみましょう:
+>>> :kind HasDefault
+HasDefault :: * -> Constraint
HasDefault
型クラスは、*
の種を持つ型を受け取り、Constraint
の種を持つ型制約を返します。Constraint
はGHCが導入している、型制約を表す種です。型制約は、GHC上ではこの 型制約種Constraint
を持ちます。つまり、HasDefault :: * -> Constraint
は、*
の種の型、つまりデータ型を一つ受け取り、型制約になるような型上の関数になります。実際に、データ型を適用して型制約にしてみましょう:
>>> :kind HasDefault Bool
+HasDefault Bool :: Constraint
適用結果は、ちゃんとConstraint
の種を持っています。ここで、種の計算では型の計算は行われないことに注意してください! kind
コマンドは、型の計算は行わないのでした。私たちは、Bool
をHasDefault
のインスタンスにしていないため、実際にはこの型制約は満たされません。型の計算を実際に行ってみましょう。上で定義したfromMaybe
を実際にBool
型に使ってみます:
>>> :type fromMaybe $ Just True
+
+<interactive>:1:1: error:
+No instance for (HasDefault Bool)
+ • of ‘fromMaybe’
+ arising from a use In the expression: fromMaybe $ Just True •
「HasDefault
はBool
に対してインスタンスを持っていない」と型エラーになっていることが分かります。このように型の計算はtype
コマンドで確かめることができるのでした。これらの型計算は、もう少し直接的に確かめることもできます。次を見てください:
>>> :set -XFlexibleContexts
+>>> :type undefined :: HasDefault Bool => Bool
+
+<interactive>:1:1: error:
+No instance for (HasDefault Bool) arising from a use of ‘it’
このように、型の計算だけを行わせる場合、undefined
を使用するのが便利です。Haskell標準では、型制約はあまり柔軟には書けません。具体的な型を伴う上のような制約も書けないため、FlexibleContexts
拡張を使用することで書けるようにしています。上の型表記で登場する、=>
という表記は、左で指定された型制約を満たしているならば右で指定された型付けの関数になる、という意味を持っています。つまり、型上の演算子として考えるなら、(=>) :: Constraint -> * -> *
という種になります。なので、例えば以下のような型表記は、種の辻褄が合わなくなります:
>>> :kind Int => Bool
+
+<interactive>:1:1: error:
+Expected a constraint, but ‘Int’ has kind ‘*’
+ • In the type ‘Int => Bool’
+ • >>> :kind HasDefault => Bool
+
+<interactive>:1:1: error:
+Expecting one more argument to ‘HasDefault’
+ • Expected a constraint, but ‘HasDefault’ has kind ‘* -> Constraint’
+ In the type ‘HasDefault => Bool’ •
kind
コマンドによって、種が合わないとエラーになっていることが分かります。残念ながら、=>
は実際には型演算子ではなく、(=>)
というように型関数として扱うことはできません。ですが、それは表記上の問題であり、確かに種の計算の際、型制約を受け取る型演算子として、Constraint
の種を持つか検査が行われていることは分かるでしょう。
さて、今までは型制約種Constraint
について、GHCi上で色々試しながら見てきました。型制約種に関しての雰囲気は分かってもらえたと思います。型制約種Constraint
をもう少し詳しく見ていきましょう。以下を見てください:
>>> data SimpleData a = SimpleData a
+>>> :kind SimpleData
+SimpleData :: * -> *
+>>> class SimpleClass a where simpleMethod :: a -> a
+>>> :kind SimpleClass
+SimpleClass :: * -> Constraint
データ宣言と型クラス宣言を並べてみました。この二つはよく似ています。
+*
という種の型になるような型コンストラクタSimpleData :: * -> *
を作り、SimpleData :: a -> SimpleData a
という値コンストラクタを作ります。Constraint
という種の型制約になるようなSimpleClass :: * -> Constraint
を作り、simpleMethod :: SimpleClass a => a -> a
というような関数を作ります。作られるものがそれぞれ違いますが、両方型の世界に一つの型関数、値の世界に関数を作るわけです。型クラスの方をデータ宣言に合わせるとしたら、型クラスは型制約コンストラクタとメソッドを作るものと言えるかもしれません。型の世界だけでの話なら、データ宣言は型コンストラクタを、型クラスは型制約コンストラクタを単に作るだけの構文ということになります。ではここで、データ型と型制約の対比を表にしてみましょう。
+種 | +表現される型 | +定義方法 | +
---|---|---|
* |
+データ型 | +データ宣言(data C a = ... ) |
+
Constraint |
+型制約 | +型クラス宣言(class C a where ... ) |
+
両者はそれぞれ特別な意味を与えられています。実際、データ型が値を持ち1、それによって型注釈が書けるように、型制約は=>
という特別に型制約を計算するような型上の構文を持っています。ですが、逆に言えば特別なのはそれだけで、それ以外に両者の違いはありません。例えば、Proxy
型は種多相化されているので、型制約や型制約コンストラクタ(型クラス)を渡すこともできます:
>>> import Data.Proxy
+>>> :kind Proxy
+Proxy :: k -> *
+>>> :type Proxy :: Proxy (Monoid Bool)
+Proxy :: Proxy (Monoid Bool) :: Proxy (Monoid Bool)
+>>> :type Proxy :: Proxy Monoid
+Proxy :: Proxy Monoid :: Proxy Monoid
=>
を使用していないので、型制約の計算は行われないことに注意してください! また、Proxy
型コンストラクタは、それぞれ以下のように特殊化されます:
Proxy :: Proxy (Monoid Bool)
の場合は、Proxy :: Constraint -> *
Proxy :: Proxy Monoid
の場合は、Proxy :: (* -> Constraint) -> *
では、このProxy
で型制約を受け取り、型制約の計算だけを行うような関数を作って使って見ましょう。その関数は、以下のように作ることができます:
>>> :set -XConstraintKinds
+>>> import Data.Proxy
+>>> -- 型制約計算を行う関数を定義
+>>> :{
+ evalConstraint :: a => Proxy a -> ()
+= ()
+ evalConstraint _ :}
+ >>> -- この段階では、まだ型制約計算されない!
+>>> Proxy :: Proxy (Monoid Bool)
+Proxy
+>>> -- 型制約計算をする関数に適用
+>>> evalConstraint (Proxy :: Proxy (Monoid Bool))
+
+<interactive>:12:1: error:
+No instance for (Monoid Bool)
+ • of ‘evalConstraint’
+ arising from a use In the expression: evalConstraint (Proxy :: Proxy (Monoid Bool))
+ • In an equation for ‘it’:
+ = evalConstraint (Proxy :: Proxy (Monoid Bool))
+ it >>> evalConstraint (Proxy :: Proxy (Monoid String))
+ ()
やっと、ConstraintKinds
拡張の登場です。ConstraintKinds
拡張は、型制約に関するHaskell標準の制限を幾つか取り払う拡張です。どのようなことが可能になるかは後で紹介するとして、今は上の関数の使い方に注目しましょう。この例のように、型制約はProxy
型で持ち回し=>
で任意のタイミングで型制約計算を行うといったことも可能です。面白いですね。
上のProxy
を使った例から明らかですが、もちろんデータ宣言時に種注釈を使うことで型制約を受け取るようなデータ型を作ることもできるわけです。そして、型制約を受け取るような型クラスも作ることができます。次の例を見てください:
>>> :set -XKindSignatures -XFlexibleInstances
+>>> import GHC.Exts (Constraint)
+>>> class AConstraint (c :: Constraint)
+>>> :kind AConstraint
+AConstraint :: Constraint -> Constraint
+>>> instance AConstraint (Monad Maybe)
FlexibleInstances
拡張は、FlexibleContexts
拡張と同じような拡張で、FlexibleContexts
は型制約の書き方の制限を、FlexibleInstances
拡張はインスタンスの書き方の制限をそれぞれ取り払う拡張です。また、Constraint
種はGHC.Exts
モジュールに入っていて、使用する際はこのモジュールをimport
する必要があります。これらを使って、上のようにすれば、型制約の分類分けすらすることができるようになります。
他にもConstraint
に関連する特殊な型上の演算子があります。普段気にも留めていなかったと思いますが、型制約のペアです。GHC上では、以下のような型制約が書けます:
>>> -- 常に型制約は満たされる
+>>> :type undefined :: () => a
+undefined :: () => a :: a
+>>> -- 二つの型制約が満たされる場合に、満たされる
+>>> :type undefined :: (Monad m, Monoid a) => m a
+undefined :: (Monad m, Monoid a) => m a
+ :: (Monoid a, Monad m) => m a
+>>> -- 以下の二つは同じ
+>>> :set -XConstraintKinds
+>>> :type undefined :: (Monad m, Monoid a, Show a) => m a
+undefined :: (Monad m, Monoid a, Show a) => m a
+ :: (Show a, Monoid a, Monad m) => m a
+>>> :type undefined :: ((Monad m, Monoid a), Show a) => m a
+undefined :: ((Monad m, Monoid a), Show a) => m a
+ :: (Show a, Monoid a, Monad m) => m a
最後の例ではConstraintKinds
拡張を使用していますが、これについては最後にどんな拡張なのか説明しましょう。今、注目してもらいたいのは、型制約のペアについてです。普段何気なく使っていると思いますが、これらも一種の型制約の演算子と見ることができるわけです。注意してもらいたいのが、この演算子はタプル型と同じ形式だということです。次を見てください:
>>> :kind ()
+() :: *
+>>> :kind (Bool, Int)
+Bool, Int) :: *
+ (>>> :kind (Monad Maybe, Monoid Bool)
+Monad Maybe, Monoid Bool) :: Constraint
+ (>>> :kind (Bool, Monad Maybe)
+
+<interactive>:1:8: error:
+Expected a type, but ‘Monad Maybe’ has kind ‘Constraint’
+ • In the type ‘(Bool, Monad Maybe)’
+ • >>> :kind (Monad Maybe, Bool)
+
+<interactive>:1:15: error:
+Expected a constraint, but ‘Bool’ has kind ‘*’
+ • In the type ‘(Monad Maybe, Bool)’ •
()
は、ユニット型の方が優先されています。(,)
は、最初に書いた型の種によって、受け取る種が左右されていることが分かりますね。型制約のペアは、(,) (Monad Maybe) (Monoid Bool)
というような表記は許容されていませんが、それ以外はあまりタプル型と変わりありません。異なるのは、タプル型が幾つかのデータ型を受け取って一つのデータ型となるのに対し、型制約のペアは型制約を幾つか受け取りそれを全て満たすような型制約になるということです。
最後にConstraintKinds
拡張をきちんと紹介しておきましょう。ConstraintKinds
拡張は、次のようなことを可能にしてくれる拡張です。
つまり、次のようなことが可能になります:
+{-# LANGUAGE ConstraintKinds #-}
+
+-- 型制約のエイリアス
+type MonMonad m a = (Monoid (m a), Monad m)
+
+-- 型制約コンストラクタのエイリアス
+type Mappable = Functor
Constraint
を持つ型を、型制約として使用できるようにする。こちらは、あまり実感が湧かないかもしれません。デフォルトで、GHCでは型クラスなどを型制約として扱う、つまり=>
に渡すことができます。ですが、Constraint
の種を持つ型制約変数などを渡すことはできません:
>>> import Data.Proxy
+>>> -- 型クラスを型制約として使っているため、問題ない
+>>> :type undefined :: Monad m => m a
+undefined :: Monad m => m a :: Monad m => m a
+>>> -- 型制約変数は、型制約として扱えない
+>>> :type undefined :: a => Proxy a
+
+<interactive>:1:14: error:
+Illegal constraint: a (Use ConstraintKinds to permit this)
+ • In an expression type signature: a => Proxy a
+ • In the expression: undefined :: a => Proxy a
+ >>> :set -XConstraintKinds
+>>> -- 型制約種を持つものなら、型制約として扱えるようになる
+>>> :type undefined :: a => Proxy a
+undefined :: a => Proxy a :: a => Proxy a
((Monad m, Monoid a), Show a)
などが標準で扱えないのも、(Monad m, Monoid a)
という形式のものは型制約種を持ってはいますが、標準で許容されている形式ではないからです。このような場合に、より柔軟に扱えるようにしてくれる拡張が、ConstraintKinds
拡張です。
型制約種Constraint
について、馴染んでもらえたでしょうか?普段、この種やConstraintKinds
を明示的に使うような場面は少ないかもしれませんね。もし、型制約種について興味を持ったなら、constraintsというパッケージを見てみるのが良いでしょう。このパッケージは、型制約プログラミングに関する幾つかの有用なAPIを提供しています。
今までは、*
やConstraint
、k1 -> k2
といった、予め用意された特別な種を紹介してきました。GHC上で、私たちが種を定義するような方法も、実は用意されています。それが、DataKinds
という拡張です。DataKinds
は基本的には簡単な拡張です。
私たちは、以下のようなデータ宣言を使ってデータ型を定義することができました:
+data SimpleData a = SimpleData a
このデータ宣言は、
+SimpleData :: * -> *
な、SimpleData a
というデータ型を作るような型コンストラクタSimpleData :: a -> SimpleData a
な、SimpleData a
というデータ型の値を作る値コンストラクタをそれぞれ作るのでした。DataKinds
は、このそれぞれのコンストラクタを、一つ上の層に昇格させることができるようになる拡張です。どういうことかは、見てみた方が早いと思うので、GHCi上でいくつか試してみます:
>>> -- 単純なデータ型を作成
+>>> data SimpleData a = SimpleData a
+>>> -- DataKinds拡張有効化
+>>> :set -XDataKinds
+>>> -- 通常のコンストラクタ
+>>> :kind SimpleData
+SimpleData :: * -> *
+>>> :type SimpleData
+SimpleData :: a -> SimpleData a
+>>> -- DataKindsによって、一つ上の層に昇格させたコンストラクタ
+>>> :kind 'SimpleData
+'SimpleData :: a -> SimpleData a
最後の実行例に注目してください。ここで書かれているSimpleData
は値コンストラクタのものです。先頭に'
(シングルクォーテーション)がついていますが、何より注目すべきなのは、種の表示にもやはりSimpleData
というものが現れていることです。これが一つ上の層に昇格させるということになります。DataKinds
拡張は、以下のようなものを提供する拡張になります:
'
を付けることにより型上で書けるようにする(型への昇格)上の実行例では、'SimpleData :: a -> SimpleData a
の、
SimpleData :: * -> *
を昇格したもの'SimpleData
が、値コンストラクタSimpleData :: a -> SimpleData a
を昇格したものになります。値コンストラクタSimpleData
は型多相化されたコンストラクタなので、それを昇格させた'SimpleData
は種多相化された型コンストラクタになります。上の実行例の、
SimpleData :: a -> SimpleData
という表示でのa
は、任意の(*
という種を持つような)型を、'SimpleData :: a -> SimpleData a
のa
は、任意の種を、それぞれ表すということに注意してください。では、種多相化されていることを確認してみましょう。以下を見てください:
+>>> :kind 'SimpleData Bool
+'SimpleData Bool :: SimpleData *
+>>> :kind 'SimpleData Maybe
+'SimpleData Maybe :: SimpleData (* -> *)
+>>> :kind 'SimpleData Monad
+'SimpleData Monoid :: SimpleData (* -> Constraint)
Proxy
型のように、どんな値でもとれるようになっていることが分かると思います。注意して欲しいのは、値コンストラクタ、型コンストラクタがそれぞれ一つ上に昇格されたので、'SimpleData Monoid
という型を持つような値は存在しないということです。値を作るコンストラクタは昇格して型コンストラクタになってしまいましたからね! 値を持つ型は全て、*
という種を持つのでしたね。SimpleData a
という種は*
と一致しないため、値を持たないということもできます。値が存在しないならば、一体どういう場面で役に立つのでしょうか? 一つの活用例としては、データ型のタグに利用ができます。以下を見てください:
{-# LANGUAGE KindSignatures #-}
+
+data GET
+data POST
+data PUT
+data DELETE
+
+data Request (a :: *) = Request String
+
+forGetMethod :: Request GET -> ...
この例は、HTTPのリクエストが、どんなメソッドでのリクエストかを、タグ情報で持つような例です。このタグ情報によって、処理を型安全に分けることができます。しかしながら、以下の問題点があります。
+Request
型も、メソッド用のタグの他にも*
という種を持っているならどんな型でも、例えばRequest Bool
といった型を作ることもできるようになってしまいます。DataKinds
を使うことで、もう少しタグ情報を明確に書くことができます。それは、以下のような修正をくわえることで、実現できます:
{-# LANGUAGE DataKinds, KindSignatures #-}
+
+data HttpMethod
+= GET
+ | POST
+ | PUT
+ | DELETE
+
+data Request (a :: HttpMethod) = Request String
+
+forGetMethod :: Request 'GET -> ...
この例では、DataKinds
拡張を使うことで、前の例での欠点を修正しています。メソッド情報はHttpMethod
というデータ型の宣言に集約していますし、種に昇格させたデータ型で種注釈を行うことで、Request
はHttpMethod
以外の型がとれないようになっています。このように、DataKinds
は値を持ちませんが、タグを表す型としてとても便利です。
その他にも、DataKinds
拡張は、シングルトンというものを定義することによって、より有用になる場合があります。ただし、これらの話は種の話題というよりは型レベルプログラミングの話題になるので、この記事では紹介しません。興味がある方は、singletonsという有用なパッケージがあるので、見てみると良いでしょう。
ところで、DataKinds
は、リスト型[a]
、タプル型(a, b)
などにも適用できます。まずリスト型の昇格から見ていきましょう。以下を見てください:
>>> :set -XDataKinds -XTypeOperators
+>>> :kind '[]
+ :: [k]
+ '[]>>> :kind '(:)
+(:) :: a -> [a] -> [a]
+ '>>> :kind Functor ': Applicative ': Monad ': '[]
+Functor ': Applicative ': Monad ': '[] :: [(* -> *) -> Constraint]
+>>> :kind '[Functor, Applicative, Monad]
+Functor, Applicative, Monad] :: [(* -> *) -> Constraint] '[
リストの値コンストラクタは二つ、[] :: [a]
、(:) :: a -> [a] -> [a]
でした。また、リストは特別な構文として、[True, False] == True : False : [] :: [Bool]
といったようなものが書けるのでした。これらをそれぞれ昇格させたものが上のものになります。タプル型の方は、以下のようになります:
>>> :kind '()
+() :: ()
+ '>>> :kind '(,)
+ :: a -> b -> (a, b)
+ '(,)>>> :kind '(Bool, Monad Maybe)
+Bool, Monad Maybe) :: (*, Constraint)
+ '(>>> :kind '(,,,)
+ :: a -> b -> c -> d -> (a, b, c, d) '(,,,)
タプル型もリスト型と大体同じような感じですね。
+さてここからは、DataKinds
のもう少し詳細な見方を紹介しておきましょう。DataKinds
は型コンストラクタを種上に昇格、値コンストラクタを型上に昇格させることをできるようにするような拡張でした。実は、Constraint
やk1 -> k2
という種も昇格された種とみなすことができます。
Costraint
の方は単純で、以下のようになっています:
>>> import GHC.Exts (Constraint)
+>>> :info Constraint
+data Constraint -- Defined in ‘GHC.Types’
見ての通り、GHC.Types
というモジュールで定義された、値コンストラクタを持たないデータ型です。この型がDataKinds
と違うところは、
Constraint :: *
が、種に昇格可能なことConstraint
型が昇格された種に結び付けられることだけで、他はDataKinds
と同じです。なので、昇格前は単純に値も型引数も持たないデータ型です。見てみましょう:
>>> import GHC.Exts (Constraint)
+>>> :kind Constraint
+Constraint :: *
確かにConstraint
が、*
を種に持つ型であることが分かりますね。
k1 -> k2
の方はちょっと特殊で、関数型コンストラクタ(->)
が昇格したものになっています。関数型は関数に結びついているデータ型でした。Constraint
の時と同じように定義を見てみると、以下のようになっています:
>>> :info (->)
+data (->) t1 t2 -- Defined in ‘GHC.Prim’
+infixr 0 `(->)`
+...
関数型もやはり値コンストラクタを持ちません。ですが、Constraint
と違い、関数型は関数という値を持ちます。Haskell上では、a -> b
という型を持つ値は、a
型の値を受け取りb
型の値を返すような関数になるのでしたね。これらの関数を作る操作、例えばラムダ記法や関数宣言などが、関数型の値コンストラクタと言えるでしょう。これらがそれぞれ昇格すると、a -> b
という種は、a
の種を持つ型を受け取り、b
の種を持つ型を返すような、型上の関数を表します。つまり、
a -> b
という関数型を、種a -> b
に昇格a -> b
という型の関数を、a -> b
という種の型関数に昇格という感じの対応をすることになります。こう見ると、少々特殊ではありますが、DataKinds
での昇格したデータ型と同じような扱いと思うことができます。
このように、Constraint
やk1 -> k2
でさえ、DataKinds
の昇格と同じように見ることができます。*
はどうでしょうか?実は、*
だけは少し特別です。見てみましょう:
>>> import GHC.Types
+>>> :kind *
+* :: *
さて、GHC.Types
モジュールには、データ型*
が定義されています。このデータ型は自身を、つまり*
の昇格された種を持っていると見ることができます。つまり、次のような型表記も可能です:
>>> import GHC.Types
+>>> :kind * -> *
+* -> * :: *
種においての* -> *
とは、上の型が種に昇格されたものとなるわけです。もちろん、次のような型表記もできます:
>>> :set -XDataKinds
+>>> import GHC.Types
+>>> :kind * -> Constraint
+* -> Constraint :: *
+>>> :kind 'Just Int
+'Just Int :: Maybe *
+>>> :kind Maybe *
+Maybe * :: *
これらをもっと視覚的にまとめてみましょう。型が結びついている種は、どのような型が昇格したものかをまとめてみると、以下のようなグラフの形になるわけです:
+'Nothing :: Maybe a
であることに注意してください。'Nothing
は種多相化されているので、'Nothing :: Maybe *
とすることも、'Nothing :: Maybe Bool
とすることも可能です2。
このように見てみると、私たちが種と呼んでいたものは、単にある型に付属する単なる型情報だと思えてきます。種注釈とは、単にその型がどういう型に付属しているかの情報に過ぎないのです。そして、値にもやはり型情報が付属しています。値とDataKinds
によって型に昇格したものを同一視してみると、値と型の間には差異はないということになりますね。このアイデアを元に、GHCでは TypeInType
という拡張が提供されています。この拡張は後ほど紹介しましょう。
最後に名前空間の話をしておきましょう。全ての種は、ある型が昇格したものである、という話をしました。GHCでは、そういう背景があり、種の名前空間は型の名前空間と完全に一致します。ただし、値の名前空間と型の名前空間を完全に一致させることはできません。それはHaskellが多用している、型コンストラクタと値コンストラクタの名前を同じにするという文化があるからです。以下の例を見てください:
+>>> :set -XDataKinds
+>>> data A a = A a | B a
+>>> :kind A
+A :: * -> *
+>>> :kind 'A
+'A :: a -> A a
+>>> :kind B
+B :: a -> A a
B
の例は、'
を書いていないのに型に昇格できていますね。DataKinds
は値と型の名前で被るものが無いようなものは'
を書かなくていいようになっています。これは型から種へ昇格できる時は自然な動作でしたが、値から型の場合、今回でいうA
のように型コンストラクタと値コンストラクタの名前が被ってしまうケースが出てきます。もちろん、データ型B
が新しく宣言されてしまうような場合もあるでしょう。このように、型から種へは名前空間が一致しますが、値から型へは名前が被るのを避けるために'
を付けるようにしているのです。なので、本質的には値から型へも、'
を付けないで昇格させることが理想です。それを覚えておきながら、DataKinds
拡張使用の際は、'
を適切に付けていくのが良いでしょう。
最後に、少し変わった型と、それにまつわる種の分類分けについてお話ししましょう。この見方は、よりGHCのプリミティブな部分に携わる時に、役にたつはずです。Haskell標準では、種は*
とk1 -> k2
しかありませんでした。GHCでは、それに型制約種Constraint
が追加されてるのでしたね。そして、DataKinds
拡張を使えば、データ型を種に昇格することもできました。しかし、結局値を持つ型は*
という種を持つのでしたね。ですが、この制約には一部例外があります。それは、GHCのプリミティブな値についてです。
私たちは普段何気なくInt
型やDouble
型を使用しています。しかしながら、これらは実際に実行するとき、メモリ上でどのような構造で保持されているのか考えたことはないでしょうか? ここでは詳細な話はしませんが、幾つか基本的なGHCでの内部表現についてお話ししましょう。GHCiで、この二つの型の情報を表示してみましょう。
>>> import Prelude (Int, Double)
+>>> :info Int
+data Int = GHC.Types.I# GHC.Prim.Int# -- Defined in ‘GHC.Types’
+>>> :info Double
+data Double = GHC.Types.D# GHC.Prim.Double#
+-- Defined in ‘GHC.Types’
どうやらこの二つの型は、不思議な値コンストラクタを持っているようですね。
+Int
データ型は、一つの値コンストラクタI# :: Int# -> Int
を持ちます。Double
データ型は、一つの値コンストラクタD# :: Double# -> Double
を持ちます。Int#
やDouble#
といった見慣れないデータ型が出てきましたね。これらが今回紹介するGHCの用意しているプリミティブなデータ型です。せっかくですから、上の値コンストラクタを使って、それぞれのデータ型の値を作ってみましょう。それにはMagicHash
拡張が必要です。通常、#
のついた値や型は、私たちは扱うことができません。それを可能にするのがMagicHash
拡張です。また、Int
やDouble
の値は、1
といった数値リテラルで作れるのでした。同じように、Int#
やDouble#
といった不思議なデータ型にも、それぞれのリテラルが用意されています。使ってみましょう:
>>> :set -XMagicHash
+>>> import GHC.Types (Int(..), Double(..))
+>>> -- I#を使って、Int型を作る
+>>> :type 1#
+1# :: GHC.Prim.Int#
+>>> I# 1#
+1
+>>> :type I# 1#
+I# 1# :: Int
+>>> -- D#を使って、Double型を作る
+>>> :type 1.0##
+1.0## :: GHC.Prim.Double#
+>>> D# 1.0##
+1.0
+>>> :type D# 1.0##
+D# 1.0## :: Double
各リテラル表記は、次のようになっています:
+Int#
のリテラルは、整数と#
で作ることができます。Double#
のリテラルは、実数と##
で作ることができます。他にも幾つか#
の付くデータ型があるのですが、まずはこのデータ型がどのようなものなのかについて、紹介しましょう。これらのデータ型は、 プリミティブ型(primitive types) と呼ばれます。そして、その多くが 非ボックス型(unboxed types) と呼ばれています。
GHCでは、多くのデータ型はボックス化、つまりヒープ上に参照データとして格納されています。データ型の値自体はポインタで、本体はヒープ上にあるというわけです。これは、データ型が、値コンストラクタを複数持つ場合もありますし、複数の様々なデータ型を取るパラメータを持つこともあり、サイズが多岐に渡るからです。しかし、ヒープに格納するには、格納する場所を計算して領域を確保し、必要なくなったら領域を解放しなければならないという、大きなコストがかかります。これは、サイズが大きなデータ型については、いちいち領域をコピーし実データのまま扱うよりも、低コストになる場合が多いですが、サイズが固定されていて尚且つ小さなデータの場合、大きな足かせになります。このため、GHCは幾つかの特別なデータ型を用意し、そのデータ型はヒープ上に格納せず直接実データとして扱うようにしています。それが、非ボックス型です。
+また、GHCは非ボックス型の他にも幾つか特別なデータ型を用意しています。Array#
やMutableArray#
、MVar#
などのデータ型です。これらは、実データとしてではなくヒープ上に格納され、ポインタをデータとするようなものです。つまり、ボックス化されているわけです。しかしながら、通常のデータ型と異なり、ヒープ上のデータは特殊な構造をしています。このように、GHCが用意している、Haskell上では定義できない特殊な構造を持つデータ型をボックス化されている/されていないに関わらず、プリミティブ型と言います。少し、まとめておきましょう:
さて、この記事は種についての記事なので、種の話もしましょう。プリミティブ型は、GHCでは*
ではなくそれぞれが特別な種を割り当てられています。見てみましょう:
>>> :set -XMagicHash
+>>> import GHC.Prim (Int#, Double#)
+>>> :kind Int#
+Int# :: TYPE 'GHC.Types.IntRep
+>>> :kind Double#
+Double# :: TYPE 'GHC.Types.DoubleRep
なにやら、不思議な種が登場しました。一体これらの種は、どのようなものなのでしょうか? info
コマンドで見てみましょう:
>>> import GHC.Types
+>>> :info TYPE
+data TYPE (a :: RuntimeRep)
+-- Defined in ‘GHC.Prim’
+ >>> :info RuntimeRep
+data RuntimeRep
+= VecRep VecCount VecElem
+ | TupleRep [RuntimeRep]
+ | SumRep [RuntimeRep]
+ | LiftedRep
+ | UnliftedRep
+ | IntRep
+ | WordRep
+ | Int64Rep
+ | Word64Rep
+ | AddrRep
+ | FloatRep
+ | DoubleRep
+ -- Defined in ‘GHC.Types’
注記: この実行例はGHC 8.2.1のものですが、これらのデータ型は現在かなりアグレッシブな変更が加えられており、表現方法がバージョンによってかなり異なります。ただし、データ型の意味は特に変わらないはずなので、手元の環境の実行例に差異があっても、特に気にしないでください!
+TYPE :: RuntimeRep -> *
、RuntimeRep :: *
、共に特に難しい定義ではありませんね。RuntimeRep
は”runtime representation”(実行環境での表現)という意味を表した名前になっています。前の章でのデータ型の昇格の話を思い出してください。TYPE 'IntRep
という種は、TYPE 'IntRep
という型が昇格したものになります。'IntRep
は、RuntimeRep
データ型の値コンストラクタIntRep
が、型に昇格したものということを思い出してください。つまり、値IntRep
を型に昇格した'IntRep
が、さらに種に昇格しているということになります。ちゃんと型が合ってるかは、昇格される前の型で調べれば良いのですね。いちよGHCiで確認してみましょう:
>>> import GHC.Types
+>>> :kind TYPE
+TYPE :: RuntimeRep -> *
+>>> :kind 'IntRep
+'IntRep :: RuntimeRep
+>>> :kind TYPE 'IntRep
+TYPE 'IntRep :: *
ちゃんと種が符合していることが分かりますね。RuntimeRep
やTYPE
は、*
やConstraint
、a -> b
などと同じく、DataKinds
無しで特別に種に昇格することが許可されています。この特別なデータ型によって、プリミティブ型は表現されています。幾つかのプリミティブ型に対する対応を、表にしてみました:
プリミティブ型 | +非ボックス型か? | +紐づいている種 | +リテラル | +C言語での型表現 | +
---|---|---|---|---|
Char# |
+o | +TYPE 'WordRep |
+文字と# ('x'# ) |
+int32_t |
+
Int# |
+o | +TYPE 'IntRep |
+整数と# (3# ) |
+int |
+
Word# |
+o | +TYPE 'WordRep |
+整数と## (3## ) |
+unsigned int |
+
Float# |
+o | +TYPE 'FloatRep |
+実数と# (3.2# ) |
+float |
+
Double# |
+o | +TYPE 'DoubleRep |
+実数と## (3.2## ) |
+double |
+
Addr# |
+o | +TYPE 'AddrRep |
+文字列と# ("foo"# ) |
+void * |
+
Array# a |
+x | +TYPE 'UnliftedRep |
+- | +- | +
MutableArray# s a |
+x | +TYPE 'UnliftedRep |
+- | +- | +
MVar# s a |
+x | +TYPE 'UnliftedRep |
+- | +- | +
この他にもGHCはプリミティブ型を用意しています。プリミティブ型は、ghc-prim
パッケージのGHC.Prim
モジュールにて公開されています。興味があれば、種を確認しながら見てみると面白いでしょう3。また、TupleRep
やSumRep
を持つ型は、それぞれUnboxedTuples
拡張、UnboxedSums
拡張を使用する必要があります。こちらについても、気になる方は調べてみてください。
さて、プリミティブ型は、その表現方法によって種が用意されていることは分かったと思います。最後に、このRuntimeRep
の中で二つの特殊な要素LiftedRep
とUnliftedRep
について話しておきましょう。データ型を表す種として、*
を紹介しました。実はこの種は、次のようなエイリアスになっています4:
type * = TYPE 'LiftedRep
つまり、今まで見てきたデータ型は、実行時にliftedという枠組みで表現されるようなものというわけです。では、このliftedとunliftedの違いはなんなのでしょうか? それの説明に入る前に、GHCにおいての評価戦略とデータの内部表現についての話をしておきましょう。ときに、Haskellは遅延評価デフォルトの言語です。例えば、以下の式の評価は例外になりません:
+>>> f :: Int -> Int; f _ = 0
+>>> f (error "raise an exception")
+0
GHCでは、このようなerror "raise an exception"
という式は評価されるまでは実際の値でなく、サンクという計算式を表現したデータとして保持されます。サンクは一度評価されると破棄され、実際の値にすげ変わります。上の関数f
が受け取るのは、このサンクまたはすげ変わった実際の値を指し示すようなポインタです。今回の場合、error "raise an exception"
という、まだ評価されていない式のサンクを指し示すポインタというわけです。上の例では、関数f
に渡されたサンクを示すポインタは、特に評価されないまま捨てられ、定数値が返ってきます。では、サンクを評価するような関数を作って、動かしてみましょう:
>>> f :: Bool -> Int; f b = if b then 0 else 1
+>>> f (error "raise an exception")
+*** Exception: raise an exception
+CallStack (from HasCallStack):
+error, called at <interactive>:2:4 in interactive:Ghci2
この例では先ほどと違い、受け取ったサンクを関数f
の中のif文で評価しています。そのため、例外が発生しているわけです。サンクはGHCが遅延評価を実装するための仕組みであり、lifted型の値は指し示す先がサンクになり得るようなポインタで表現されます。lifted型とは、その値としてボトムと称される値になるようなものも持てるような型のことです。ボトムと呼ばれる値には、主に以下のようなものがあります:
Haskellの通常の型は、上のような式を表すサンクをも値として持ちますから、無限ループになったり例外が出されたりするような値、つまりボトムをも表現できます。それに対して、GHCではボトムを表現できない型も存在します。その典型が非ボックス型です。非ボックス型はポインタではなく、実データとして表されているんでしたよね。実データは、サンクとすげ替えるということができないですよね。では、非ボックス型を処理するような関数に、undefined
を渡すとどうなるか、見てみましょう:
>>> :set -XMagicHash
+>>> import GHC.Exts
+>>> f :: Int# -> Int; f _ = 0
+>>> f (error "raise an exception")
+*** Exception: raise an exception
+CallStack (from HasCallStack):
+error, called at <interactive>:7:4 in interactive:Ghci4
先ほどのliftedの例(Int
の例)と、何が違うか分かりますか? 今回、関数f
はやはり受け取った値を無視して定数を返します。どこにも受け取った引数を評価する箇所はありません。
Int
の例)では、undefined
を評価せず、つまり例外が一切出ずに定数が返ってきました。Int#
の例)では、例外が発生しています。なぜでしょうか? 実は、非ボックス型は遅延評価ではなく正格評価が行われます。その理由はお分かりですね? なぜなら遅延評価のためにサンクを用意しようにも、非ボックス型はサンクを表現できないからです! 非ボックス型として値を格納するならば、サンクではなく評価した後の実データでないといけません。そのため、ボックス型の引数を受け取る関数の場合は、一旦引数に渡される式を評価して実データにした上で、関数に渡すということを行います。実は、これは非ボックス型だけに止まりません。GHCでは、ボックス型の中にも正格評価になるような、つまりサンクを値として持たないような型があります。それが、TYPE 'UnliftedRep
を種に持つデータ型です。見てみましょう:
>>> :set -XMagicHash
+>>> import GHC.Exts
+>>> f :: Array# a -> Int; f _ = 0
+>>> f (error "raise an exception")
+*** Exception: raise an exception
+CallStack (from HasCallStack):
+error, called at <interactive>:8:4 in interactive:Ghci2
Array# a
はヒープ上に本体があり、それを指し示すポインタで表現されます。ただしこのポインタは、サンクを指し示すことはありません。つまりかならず実データを指し示すことになり、ボトムを値に持つことはないのです。
ところで、今までは引数がunliftedな型である場合の話をしてきましたが、返り値がunliftedな型になっている場合はどう見ることができるのでしょう?例えば、次のような関数を考えてみてください:
+{-# LANGUAGE MagicHash #-}
+
+import GHC.Exts
+
+infLoop :: Int# -> Int#
+= infLoop (i +# 1#) infLoop i
(+#) :: Int# -> Int# -> Int#
は、GHCで用意されているInt#
専用の加算演算子です。この関数は問題なく定義することができますが、実行すると無限ループを起こします。つまりinfLoop 1# :: Int#
というような式はボトムを表しているように見えます。unliftedな型は、ボトムを持たないはずでは無かったのでしょうか? 注意して欲しいのは、infLoop 1#
という式は、それ単体ではHaskellでは単なる表記に過ぎないということです。この式は、なんらかのトップレベル関数や定数の一部になっているはずです。関数はliftedな型の値です(関数型は、a -> b :: *
であることを思い出してください!)。関数はコンパイルされ、ランタイムによって実行されます。つまり、最終的に実行時に意味を持つのは、トップレベルの関数であり、それはliftedな型で表現されるということです。また、Haskellではunlifted型のトップレベル定数の宣言は許されていません。以下のコードはコンパイルエラーになります:
{-# LANGUAGE MagicHash #-}
+
+import GHC.Exts
+
+-- 許可されていない
+unliftedConstant :: Int#
+= 1# unliftedConstant
これにより、トップレベルの関数や定数は、全てliftedな型を持つことになります。もし、内部でunliftedな式が無限ループや例外を吐くなら、それはその式を含んだトップレベルのliftedな関数や定数が、ボトムを表すサンクを持つことになるということです。これは、unliftedの考え方を逸脱しません。
+このような解釈によって、Haskellでのlifted/unliftedの枠組みは保たれます。
+ボトムについての形式的な議論は、領域理論という分野でされています。もし、lifted/unliftedについての理論的な背景が知りたいなら、領域理論や表示的意味論について学習してみると良いでしょう5。
+ここまでのことを大雑把にまとめておきました。GHCでは型について幾つかの大別をしています:
+なお、非ボックス型はunliftedであり、liftedな型はボックス型になります6。では、幾つかの型の種別を見て、今回は終わりにしましょう(type * = TYPE 'LiftedRep
であることに注意してください!):
型名 | +種 | +プリミティブ型か | +ボックス型か | +liftedか | +
---|---|---|---|---|
Bool |
+TYPE 'LiftedRep |
+x | +o | +o | +
Int# |
+TYPE 'IntRep |
+o | +x | +x | +
Array# a |
+TYPE 'UnliftedRep |
+o | +o | +x | +
この章では、型制約を表す種Constraint
の紹介、型を種に、値を型に昇格するDataKinds
拡張の紹介、そしてプリミティブ型の種とGHCの型の大別について、お話ししました。
型制約には型制約種Constraint
という種がつくのでした。データ宣言が型コンストラクタと値コンストラクタを作るように、型クラスは型制約コンストラクタと型制約下でのメソッド群を作るものとみることができました。また、型制約は、=>
によって制約が満たされるか検査されるのでしたね。ただ、Haskell標準では型制約は決まった形状でしか書けませんでした。そのため、ConstraintKinds
拡張が用意されており、この拡張によって型制約種を持つものならば変数であろうと型制約のペアであろうと、型制約として扱えるようになるのでした。また、この拡張によって、型制約のエイリアスも書けるようになりました。
DataKinds
はデータ型の型コンストラクタを種において使えるように、値コンストラクタを型において使えるようにするものでした。値コンストラクタは、昇格の際先頭に'
をつけるのでした。また、Haskellの種全般が、何かしらの型が昇格したものとみなせるという話もしましたね。*
ですら、一つのデータ型でした。
最後に、GHCのプリミティブ型、ボックス型、lifted型という大別を紹介しました。
+Int#
、Array#
などがそうです。undefined
などの評価すると例外になるようなものや無限ループでさえ値として持ち得るのでした。unlifted型の値は、サンクを持たず、正格に評価されるのでした。以降では、少し高度な種に関する話題を紹介していきます。あまり知られてない機能や最近入った機能、まだ入ってない提案中のものなども紹介していきます。これらの話題は、最初に掲げた想定読者層から外れているのであまり詳しくは紹介しません。こんな話もあるんだぐらいに留めておいてもらえれば、良いでしょう。
+今までは、種に関する基本的な話題を紹介しました。ここでは、種とは別の、もう一つの型に付属する種別情報を紹介しましょう。それは、type roleと呼ばれるものです。type roleは、GeneralizedNewtypeDeriving
という拡張と、密接な関係があります。
ここでは詳しく解説しませんが、GeneralizedNewtypeDeriving
という拡張は、newtype
で作った型のクラスインスタンス導出を簡略化するための拡張で、そのインスタンスを元の型のものを持ってきて実装します。この拡張は利便性を向上させますが、その導出が壊れるケースが出てきます。例えば次のケースです:
{-# LANGUAGE GeneralizedNewtypeDeriving, StandaloneDeriving, TypeFamilies #-}
+
+newtype Age = MkAge { unAge :: Int }
+
+type family Inspect x
+type instance Inspect Age = Int
+type instance Inspect Int = Bool
+
+class BadIdea a where
+ bad :: a -> Inspect a
+
+instance BadIdea Int where
+= (> 0)
+ bad
+deriving instance BadIdea Age -- 壊れた導出になる
+{- | 上のものは、以下のものと同じ
+instance BadIdea Age where
+ bad = coerce (bad :: Int -> Inspect Int)
+-}
Age -> Inspect Age
はAge -> Int
と同じ、Int -> Inspect Int
はInt -> Bool
と同じであるということに注意してください。この場合、Age
とInt
は同じ実行時表現を持ちますが、Int
とBool
は同じ表現を持たないわけですから、直感的にはInt -> Inspect Int
をAge -> Inspect Age
にキャストすることは型の健全性を壊します。
type roleは、二つの型が同じ表現を持つ型かどうかを判断するために、組み込まれた機能です。つまり、キャストが型安全にできるかを判断するためのものなのです。データ型や型クラス、型族などの型変数は、type roleを持ちます。type roleの概念は、a
とb
という型が同じ表現を持つときに(例えば、Int
とAge
は同じ表現を持ちます)、型コンストラクタや型族T
に対してT a
とT b
が同じ表現を持つかを判断するための機構で、T
のどのパラメータが判断するときに関与するか、という情報を持ちます。type roleは以下の三種類があります:
nominal: 受け取ったパラメータの型が例え同じ表現であっても、全体として同じ表現になるとは限らないことを示します:
+type family F a -- a has nominal type role
+
+data D a = D (F a) -- a has nominal type role
representational: 受け取ったパラメータの型が同じ表現であるならば、全体としても同じ表現になることを示します。一般的にはこのtype roleを持ちます:
+data Simple a = Simple a -- a has representational type role
phantom: 受け取ったパラメータが、表現に全く関与しないことを示します:
+data Tag a = Tag -- a has phantom type role
これらのtype roleは推論によって決定するようになっているため、私たちは普段特に意識する必要はありません。ただし、推論結果が意図しないものである場合もあります。その際は、RoleAnnotations
拡張を使って、以下のようにすることで、type roleを明示的に書くこともできます:
type role T nominal _ phantom
+data T a b c = T Int b
_
は推論に任せることを意味します。
type roleに興味があるならば、GHC User’s Guide - 9.36 Rolesを読んでみると良いでしょう。また、歴史的経緯については、Roles: a new feature of GHCに簡潔にまとまっています。
+前の章では、プリミティブ型の種を紹介しました。この種はTYPE :: RuntimeRep -> *
という型コンストラクタを昇格したものによって、作られているのでした。そして、*
でさえTYPE 'LiftedRep
のエイリアスでしか無かったのでしたね。このような種の表現になったのは、実は最近のことです。昔はそれぞれの種は実行時の表現ごとに切り離されていました。TYPE
を使って共通化されたのには、幾つかの歴史的経緯があり、 軽率多相(levity polymorphism) という話題と密接な関係があります。軽率多相は、種多相を少し制限したものです。種多相は任意の種を扱えるような種変数を許容しますが、軽率多相は実行時表現に関係するような範囲での種の多相を提供します。
関数型コンストラクタ(->)
の種は、(->) :: * -> * -> *
であると話してきました。実際には、(->) :: TYPE q -> TYPE r -> *
というような種を持っています。TYPE
の引数の部分は、任意のRuntimeRep
をとれるようになっています。確認してみましょう:
>>> :set -XTypeInType -XKindSignatures
+>>> import GHC.Exts
+>>> type CheckFuncType (q :: RuntimeRep) (r :: RuntimeRep) (a :: TYPE q) (b :: TYPE r) = a -> b -- no error
TypeInType
拡張については、後ほど解説しますので、今はおまじないとしておきましょう。上の例では、任意のRuntimeRep
に対して、確かに関数型コンストラクタが有効であることが確認できます。このように関数型コンストラタは軽率多相化されています。なので、私たちはInt# -> Int#
といったような関数を、通常の関数型の表記で書けるようになっていたのです。
関数型が軽率多相化されているということは、軽率多相化された関数が書けるということでもあります。実際、幾つかの関数は軽率多相化された形で提供されています。例えば($)
やerror
などがそうです。ただし、軽率多相は全てに気軽に適用できるというわけではありません。以下の例を考えて見ましょう:
{-# LANGUAGE TypeInType, KindSignatures, ExplicitForAll #-}
+
+import GHC.Exts (RuntimeRep, TYPE)
+
+bad :: forall (r1 :: RuntimeRep) (r2 :: RuntimeRep)
+a :: TYPE r1) (b :: TYPE r2).
+ (-> b) -> a -> b
+ (a = f x bad f x
この例は、($)
を軽率多相化した例になります。しかし、この実装には幾つかの問題があります。bad
をコンパイルすることを考えてみてください。bad
の引数f
はただの関数なので問題ありませんが、引数x
は軽率多相化されています。x
は果たして実データかポインタかどちらでしょうか? また、サンクを持っているのでしょうか? x
のビット幅は? x
はどのレジスタに格納すべきでしょうか? 私たちはコンパイル時に、この質問に答えることはできません。なぜなら実データである場合もありポインタである場合もありますし、サンクを持っているかもしれません。浮動小数点数である場合も整数である場合もあり、ビット幅も一定ではないからです! つまり私たちは、bad
の引数x
をどのように扱えばいいのか、コンパイル時に決めることができないのです。これは、引数が軽率多相化された関数全てに対して当てはまることです。このため、GHCでは軽率多相化したような変数を使った関数宣言は許可されていません。ですが、注意して欲しいのは、次のような関数は作ることができるということです:
{-# LANGUAGE TypeInType, KindSignatures, ExplicitForAll #-}
+
+import GHC.Exts (RuntimeRep, TYPE)
+
+good :: forall (r1 :: RuntimeRep) (r2 :: RuntimeRep)
+a :: TYPE r1) (b :: TYPE r2).
+ (-> b) -> a -> b
+ (a = f good f
この場合、軽率多相化された変数はどこにも出てきていません。
+ところで、軽率多相の焦点は、実行時表現の中でもlifted/unliftedの枠組みについてです。liftedな型の値はサンクを持ち遅延評価を主とし、unliftedな型の値はサンクを持たないので正格評価になるのでしたね。このサンクを持つ持たないに関わらず関数を多相化して書けるようにするのが、軽率多相の主な目的です。ですが、GHCではもう一つ重要なboxed/unboxedという枠組みもあるのでしたね。この二つの枠組みを分けて多相化できるように、現在次のような変更も提案されています:
+data Boxity = Boxed | Unboxed
+data Levity = Lifted | Unlifted
+data TYPE (b :: Boxity) (l :: Levity)
+
+type * = TYPE 'Boxed 'Lifted
もし、軽率多相に興味があるならば、原論文Levity Polymorphism (extended version)を読んでみるのがいいでしょう。この論文では、軽率多相に至るまでのGHCでの経緯と問題点、軽率多相の動機などが丁寧に解説されています。また、論文より説明が若干劣りますがGHC User’s Guide - 9.12 Levity polymorphismにも、GHCでの軽率多相についての仕組みが書かれています。
+KindSignatures
拡張は、種の注釈を書けるようにするようなものでした。KindSignatures
は型パラメータの種を明示的に書くことができるようにする拡張でしたね。種が明示されないパラメータは、種推論によってその種が定まります。ですが、種推論は幾つか制約があり、その一つに再帰的データ型に対しては、単相的な再帰の種推論しか行えないというものがあります。以下のケースを見てください:
>>> :set -XPolyKinds
+>>> data T m a = Nil | MkT (m a) (T Maybe (m a))
+>>> :kind T
+T :: (* -> *) -> * -> *
このデータ型T
の種は(k -> *) -> k -> *
というような多相化された種でも問題ないはずですが、実際にはPolyKinds
をつけているにも関わらず*
で単相化されて推論されます。単相制約によって、例えば次のように、一部種注釈を書いてもうまく推論できません:
>>> data T (m :: k -> *) a = Nil | MkT (m a) (T Maybe (m a))
+
+<interactive>:49:45: error:
+Expected kind ‘k -> *’, but ‘Maybe’ has kind ‘* -> *’
+ • In the first argument of ‘T’, namely ‘Maybe’
+ • In the type ‘T Maybe (m a)’
+ In the definition of data constructor ‘MkT’
+ >>> data T m (a :: k) = Nil | MkT (m a) (T Maybe (m a))
+
+<interactive>:50:40: error:
+Expected kind ‘k -> *’, but ‘Maybe’ has kind ‘* -> *’
+ • In the first argument of ‘T’, namely ‘Maybe’
+ • In the type ‘T Maybe (m a)’
+ In the definition of data constructor ‘MkT’
ですが、私たちは完全に種推論に頼らないような種注釈を提供することで、T :: (k -> *) -> k -> *
というような種多相化された型コンストラクタを作ることができます:
>>> data T (m :: k -> *) (a :: k) = Nil | MkT (m a) (T Maybe (m a))
+>>> :kind T
+T :: (k -> *) -> k -> *
このような完全に種が提供されているような種注釈の形式を、GHCでは CUSKs(Complete User-Supplied Kind signatures) と呼んでいます。CUSKは、種多相な再帰的データ型を提供する場合必須のものになってきますが、上の例からも分かる通り非常に見にくいのが難点です。また構文を解析してCUSKか判断するのにも、手間がかかります。そのため、現在CUSKに代わるものとして、 トップレベル種注釈(top-level kind signatures) という機能が提案されています。この提案は、上の例のCUSKと同等の注釈を、次のように書けるようにするものです:
+type T :: (k -> *) -> k -> *
+data T m a = Nil | MkT (m a) (T Maybe (m a))
関数の型注釈などと同じスタイルで、非常に見やすいですね。
+トップレベル種注釈は、現在、GHC Proposals - Pull Request #54で提案されています。興味がある方は、GHC User’s Guide - 9.11.5 Complete user-supplied kind signatures and polymorphic recursionと合わせて提案内容を見てみると良いでしょう。
+Haskellのデータ型は、liftedという枠組みの型でした。liftedな型は、評価されるまではサンクになっているのでした。unliftedな型は、GHCで幾つかプリミティブ型として提供されているのでした。現在、unliftedデータ型という拡張が提案されています。この拡張は、ユーザー定義のunliftedな型を定義できるようにする拡張です。
+この拡張は、次のような新たなデータ宣言をできるようにするものです:
+data unlifted UBool = UTrue | UFalse
ここで、UBool
型の値は、unliftedな型でありボックス型であるようなデータ型になります。つまり、Array# a
と同じようなデータ型で、サンクを持たずポインタでヒープ上の本体を指し示すような表現がされます。そして、その種はTYPE 'UnliftedRep
になります。また、この拡張下では、unlifted型のnewtype
が行えるようにするという提案もされています。
ただこの拡張は新たなシンタックスを導入することになるため、GADTSyntax
とKindSignatures
を使って以下のようなことをできるようにすることが、代わりに提案されています:
{-# LANGUAGE GADTSyntax, KindSignatures #-}
+
+data UBool :: TYPE 'UnliftedRep where
+UTrue :: UBool
+ UFalse :: UBool
これにより、新たな構文を導入しなくても、unliftedなデータ型を定義できるようになります。
+unliftedデータ型について興味があるならば、GHC Wiki - UnliftedDataTypesのページを見てみると良いでしょう。このページに、主な提案内容が書かれています。
+TypeInType
拡張は、DataKinds
拡張とPolyKinds
拡張をより強力にした拡張です。例えば、以下のようなことができるようになります:
>>> :set -XTypeInType -fprint-explicit-foralls
+>>> -- 型エイリアスを種として使えるようになる
+>>> type B = Bool
+>>> data D (a :: B)
+>>> :kind D
+D :: B -> *
+>>> -- より広い範囲の多相を書けるようになる
+>>> data A (d :: D a)
+>>> :kind A
+A :: forall (a :: B). D a -> *
+>>> -- RankN多相な種を書けるようになる
+>>> :set -XRankNTypes
+>>> data AN (d :: forall a. D a)
+>>> :kind AN
+AN :: (forall (a :: B). D a) -> *
このように、型注釈でできたことが、種注釈でできるようになるわけです。
+ただし、この拡張は現状とても不安定であり、使用が推奨されているわけではありません。この拡張を使用する場合は、コンパイル時、内部でこの拡張の挙動をチェックするように-dcore-lint
というフラグを使用することが推奨されています。将来的には、この拡張の範囲を型と種の範囲から、型と値の範囲、ひいては種と型と値の違いを取り払い、依存型というシステムに徐々に近づけていくことも視野に入れているようです。ただし、まずは種と型の範囲で安定的な機能を提供するのが、目的ということでしょう。
TypeInType
拡張については、主にGHC User’s Guide - 9.11.3 Overview of Type-in-Typeに、その概要が書かれています。
この章では、幾つかの種に関する話題をかいつまんで紹介しました。
+type roleは、二つの型が同じ内部表現を持つ型かを判定するための、型変数が持つ種とは異なる情報でした。nominal/representational/phantomの三種があり、データ宣言でどのtype roleを割り当てるかが推論されるのでした。また、RoleAnnotations
拡張によって明示的に指定することも可能なのでした。
軽率多相(levity polymorphism)はlifted/unliftedの違いを吸収する多相でした。これによって、lifted/unliftedの違いを問わない多相化された関数を書けるようになるのでした。ただし、全ての関数を制限なく軽率多相化することはできず、関数の引数が軽率多相化されているようなものは、機械語にうまく翻訳できないため書けないのでした。
+トップレベル種注釈は、既存のCUSKという種注釈の方法に変わり、種注釈を分かりやすく書くための提案でした。再帰的データ型に対して種推論がうまく働かないという制約から、多相的な種を使用する場合、種推論に頼らず完全な種の情報を提供する必要があり、完全に情報が提供されるような種の注釈をGHCではCUSKと読んでいるのでした。しかし、CUSKは一般的に見づらいため、それを解決するための提案でしたね。この提案されている構文は、関数の型注釈と同じスタイルで非常に分かりやすいですね。
+unliftedデータ型は、unliftedな型を定義できるようにするような拡張として提案されているものでした。この拡張により、ボックス型でかつunliftedなもの、つまりサンクを持たないボックス型を定義できるようになるのでした。現状、幾つか議論されるべき課題が残っていますが、近い将来導入されるかもしれません。
+TypeInType
は型と種の違いを取り払うような拡張でした。これにより、型エイリアスを種に昇格したり、RankNの種注釈を書けるようになるのでした。ただし、現状は非常に不安定であり、使用には注意が必要です。
さて、Part 1と合わせて、一通りの種に関する話題を紹介してきました。種の仕組みの紹介と5つの大きな話題を取り扱ってきました。また、少し高度な話題を幾つか、駆け足で紹介しました。この二つの記事が、何かの役に立てば幸いです。
+もし、Advanced Topicsの内容について、詳細が知りたいという声が多ければ、Part 3を書くかもしれませんが、ひとまずはこれで。では、良いHaskellライフをノシ
+DataKinds
拡張の動機と解説が書かれています。PolyKinds
拡張に関する話題がまとまっているページです。OpenKind
というものがありました。しかしながら、この仕組みは幾つか問題が知られており、現在は軽率多相によって置き換えられています。ここでは、OpenKind
の仕組みと問題点、その解決法が書いてあります。DataKinds
拡張の提唱論文です。DataKinds
について紹介する時、参考にしました。型コンストラクタは値を持てないことに注意してください! 何らかの値を持つ型は全て*
という種を持つものになっており、例えばMaybe :: * -> *
という型コンストラクタはそれだけでは値を持たず、Maybe Int
など型を一つ渡して初めて値を持つような型になるのでした。↩︎
ここでのBool
は、Bool
型が種に昇格したものという点にも注意してくださいね!DataKinds
拡張によって、データ型は種に昇格できるのでした。↩︎
VecRep
を持つプリミティブ型は紹介しませんでしたが、この型はSIMDベクトル演算を利用するために用意されています。VecCount
はレーン数、VecElem
はSIMD APIのどのデータ型を使用するかを表します。これらのプリミティブ型はSIMD Vectorsの章にまとまっているので、興味があれば見てみると良いでしょう。↩︎
この種の定義は、GHC.Typesモジュールで確認することができます。他にも、エイリアスとしてType
やUnicode版の★
が用意されています。↩︎
あなたがもし領域理論について興味があるならば、Domain Theoryを読んでみるのがよいでしょう。この文献は、領域理論に必要な順序理論の知識から、領域理論の基本的な概念を解説してくれている文献です。もし、理論自体に興味がなく、この理論がどのような問題解決を目指しているかだけを知りたいなら、Originsだけでも読むと良いでしょう。↩︎
非ボックス型は実データなので、サンクはどうやったって持てないんでしたね! 逆にliftedならば必ずポインタで表されているはずなので、ボックス型になります。ただし、unliftedだからといって非ボックス型とは限りません(例: Array# a
)。また、ボックス型だからといってliftedであるとは限りません。↩︎
これは Haskell Advent Calendar その4 の12日目の記事です。
+なんで12日目の記事で「まとめ」をやっているのかって? +25日を過ぎてから余ってた日付に登録しただけですよ。
+QiitaとADVENTARのアドベントカレンダーで投稿された、Haskellに関する記事を集めてみました。 +いちおう機械的に集めたので、それなりに拾えてると思いますが、もし「この記事が無いよ」とか、逆に「Haskellちゃうやんこの記事」ってのがあったら、ここから編集してPRでも送ってください。
+ちなみに「Elm Advent Calendar 2017 まとめ」という記事が面白かったので、そのHaskell版オマージュ(パクリ)です。
+Qiitaの方では4つもできましたね。
+まぁしかし、残念ながらどのカレンダーも埋まってないためランキング圏外となってしまいましたが(笑)
+加えて何故か、25日が過ぎてからその5のカレンダーができるという、なかなか面白い事案が発生しました。
+ +上記のカレンダーの記事以外も含めて、全部で104記事もありました。 +みんなすごいですね。
+超雑にスクレイパーを書いてQiitaとADVENTARのカレンダーをスクレイピングしてきました。 +プログラムは以下のリポジトリにあります(もちろんHaskellで作ったよ)。
+ +カレンダーのタイトルか、記事のタイトルに「Haskell」って単語が入っているやつだけ集めてます。 +このプログラムの解説っぽい記事はそのうち自分のとこの記事として挙げる気がする(たぶん)。
+分類は温もりのある手作業でやってます。 +自然言語処理系が出来れば機械的に分類できたかもしれませんが…
+分類違くね?というモノがあれば、ここから編集してPRでも送ってください。
+それでは良いお年を。
+なぜ Haskell が好きなのか - 趣味はデバッグ……
+by kakkun61 on Haskell (その3) Advent Calendar 2017 - Qiita 12/06
Haskell副作用ポエム - Qiita
+by Mizunashi_Mana on Haskell (その4) Advent Calendar 2017 - Qiita 12/21
Haskellを勉強して感動したこと・難しいと思ってること - Qiita
+by ababup1192 on Haskell Advent Calendar 2017 - Qiita 12/04
Haskell入門しようとして環境構築で失敗。 · GitHub
+by sys9kdr on Haskell (その3) Advent Calendar 2017 - Qiita 12/05
ClojurianがHaskellでWeb API開発に入門してみた - Qiita
+by lagenorhynque on Haskell (その3) Advent Calendar 2017 - Qiita 12/19
美術の人が考える Haskell - Qiita
+by hitsujisanmeme on Haskell (その2) Advent Calendar 2017 - Qiita 12/14
Haskell入門者がライブラリを触っちゃう!? - Qiita
+by brackss1 on Haskell Advent Calendar 2017 - Qiita 12/22
Ubuntu、Haskellでwebアプリ手始め - Qiita
+by ryosukue on Nuco Advent Calendar 2017 - Qiita 12/23
Haskellをちょこっと紹介(フィボナッチ数列を書いてみる) - Qiita
+by 3nan on TECOTEC Advent Calendar 2017 - Qiita 12/23
私のHaskellコーディングスタイルガイド,改行出来るポイントを紹介 - ncaq
+by ncaq on Haskell (その3) Advent Calendar 2017 - Qiita 12/02
ゲーム販売webアプリケーションSYAKERAKEを支える技術,HaskellとYesodで作られています - ncaq
+by ncaq on Haskell (その3) Advent Calendar 2017 - Qiita 12/03
Stackage Serverのちょっとした便利な使い方,Hoogleをブラウザのカスタム検索エンジンに追加しましょう,よく使うパッケージをブックマークする時はPackageRのURLにしましょう - ncaq
+by ncaq on Haskell (その3) Advent Calendar 2017 - Qiita 12/04
goな関数
+by kazu_yamamoto on Haskell (その2) Advent Calendar 2017 - Qiita 12/01
HaskellのABC(Haskell Advent Calendar 6th) - モナドとわたしとコモナド
+by fumieval on Haskell (その2) Advent Calendar 2017 - Qiita 12/06
WindowsでHaskellを扱う時によく遭遇するエラーと対処法 - Haskell-jp
+by igrep on Haskell (その4) Advent Calendar 2017 - Qiita 12/14
A Tour of Go in Haskellを作ったのと、GoとHaskellの比較 - syocy’s diary
+by syocy on Haskell (その2) Advent Calendar 2017 - Qiita 12/03
ServantのハンドラにReaderTを適用する - Qiita
+by cyclone_t on Haskell (その2) Advent Calendar 2017 - Qiita 12/15
スーパーモナドについて - Qiita
+by wgag on Haskell Advent Calendar 2017 - Qiita 12/10
CircleCI2.0でHaskellのテストを実行する – PSYENCE:MEDIA
+by yukiasai on RECRUIT MARKETING PARTNERS Advent Calendar 2017 - Adventar 12/13
Dokku環境を構築してHaskellのアプリケーションをデプロイする - Qiita
+by yukiasai on Recruit Engineers Advent Calendar 2017 - Adventar 12/07
Haskell Stack とは何をするツールなのか - Qiita
+by usamik26 on Haskell (その2) Advent Calendar 2017 - Qiita 12/12
stack ls コマンドが追加されます
+by waddlaw on Haskell (その4) Advent Calendar 2017 - Qiita 12/08
stack v1.6.3 がリリースされました。
+by waddlaw on Haskell (その4) Advent Calendar 2017 - Qiita 12/09
Haskellや周辺ツールについてのリンク集 - Qiita
+by ogata-k on Haskell (その4) Advent Calendar 2017 - Qiita 12/15
Haskellのstackによるプロジェクトについて - Qiita
+by ogata-k on Haskell (その4) Advent Calendar 2017 - Qiita 12/16
haddock に Grid Table 記法が追加されました
+by waddlaw on Haskell (その5) Advent Calendar 2017 - Qiita 12/02
カスタムスナップショットの紹介
+by waddlaw on Haskell Advent Calendar 2017 - Qiita 12/14
Haskellプロジェクトを始めるにあたって - The curse of λ
+by myuon_myon on 一人Computer Science Advent Calendar 2017 - Qiita 12/15
servant+persistentを利用する - Qiita
+by jabaraster on Haskell (その3) Advent Calendar 2017 - Qiita 12/07
YampaでFunctional Reactiveな認知行動療法ボット - Qiita
+by makoraru on Haskell (その3) Advent Calendar 2017 - Qiita 12/13
Haskellで機械学習を実装しようと思った過去の自分へ
+by _Nnwww on Haskell (その2) Advent Calendar 2017 - Qiita 12/23
Haskell・Servant+Persistent/Esqueletoで作る実用WebAPI (1) Servantの基本 - Qiita
+by cyclone_t on Haskell (その4) Advent Calendar 2017 - Qiita 12/11
Extensible Effects ステップ・バイ・ステップ
+by matsubara0507 on Haskell Advent Calendar 2017 - Qiita 12/09
Haskellのパーサライブラリまとめ - Qiita
+by Mizunashi_Mana on Haskell Advent Calendar 2017 - Qiita 12/11
Haskell入門者LT会で自作ライブラリnetwork-voicetextの話をしてきた | ザネリは列車を見送った
+by zaneli@github on Haskell Advent Calendar 2017 - Qiita 12/13
Haskell と SQLite - Qiita
+by satosystems on Haskell Advent Calendar 2017 - Qiita 12/16
部分構造の共有を観測するdata-reify - Qiita
+by masahiro_sakai on Haskell Advent Calendar 2017 - Qiita 12/21
Elm と Haskell で作る ToDo アプリ
+by matsubara0507 on Elm Advent Calendar 2017 - Qiita 12/13
Haskellに入門して1年位経ったのでライフゲームを作ってみた話 - abc10946の日記
+by ABC10946 on Haskell (その3) Advent Calendar 2017 - Qiita 12/18
仕事でHaskellを使いたいなら外堀から
+by hxf_vogel on Haskell (その3) Advent Calendar 2017 - Qiita 12/22
slack-api + bloodhound + servant でbot+αを作る
+by nakaji-dayo on Haskell (その2) Advent Calendar 2017 - Qiita 12/07
【Haskell (その2) Advent Calendar 2017】Vim から Hoogle の検索が出来るプラグインをつくった【16日目】 - Secret Garden(Instrumental)
+by pink_bangbi on Haskell (その2) Advent Calendar 2017 - Qiita 12/16
お天気Bot で理解する Haskell の便利パッケージ - Qiita
+by rounddelta on Haskell (その2) Advent Calendar 2017 - Qiita 12/25
Haskell ochintin-daicho で年末調整プログラミング - Qiita
+by arowM on Haskell Advent Calendar 2017 - Qiita 12/12
作って学ぶBitcoin!ゼロから作るSPVウォレット - Qiita
+by lotz on Haskell Advent Calendar 2017 - Qiita 12/18
優秀な秘書を雇いました!!! - Creatable a => a -> IO b
+by tokiwoousaka@github on Haskell Advent Calendar 2017 - Qiita 12/20
Haskell における遅延ファイル読み込みとリソースリーク - Qiita
+by satosystems on Haskell Advent Calendar 2017 - Qiita 12/23
タイプセーフプリキュア!を支える技術 その2 - Haskell-jp
+by igrep on Haskell Advent Calendar 2017 - Qiita 12/24
Haskellによるwebスクレイピングの方法をdic-nico-intersection-pixivを例に書く - ncaq
+by エヌユル on Webスクレイピング Advent Calendar 2017 - Adventar 12/19
Re: ゼロから作る ADVENTAR の Slack Bot (Haskell 編)
+by ひげ on 群馬大学電子計算機研究会 IGGG Advent Calendar 2017 - Adventar 12/02
GHCの中間言語Coreへの脱糖を覗き見る - Hash λ Bye
+by ilyaletre on Haskell (その3) Advent Calendar 2017 - Qiita 12/11
Haskellの型システムを書く(1) - 純粋技術メモ
+by fujiy on Haskell Advent Calendar 2017 - Qiita 12/03
GHCでの中置演算子のパース - Qiita
+by takoeight0821 on Haskell Advent Calendar 2017 - Qiita 12/06
型システムを学ぼう!
+by unnohideyuki on Haskell Advent Calendar 2017 - Qiita 12/15
Haskell Backpack 覚え書き
+by matsubara0507 on Haskell (その3) Advent Calendar 2017 - Qiita 12/12
にこ、希と一緒に学ぶHaskell(番外)「あまり知られていないGHC拡張の紹介」 - Qiita
+by aiya000 on Haskell (その3) Advent Calendar 2017 - Qiita 12/21
Levity polymorphismについて軽く - Qiita
+by ruicc on Haskell (その2) Advent Calendar 2017 - Qiita 12/02
Kindについて - Qiita
+by ryoppy on Haskell (その2) Advent Calendar 2017 - Qiita 12/11
OverloadedLabels と Haskell Relational Record - khibino blog
+by khibino on Haskell (その2) Advent Calendar 2017 - Qiita 12/18
GHC 8.2 以前で FFI を使う際に注意すること - Qiita
+by thimura on Haskell Advent Calendar 2017 - Qiita 12/19
依存型の紹介と応用としてのClashの紹介 - Qiita
+by junjihashimoto@github on Haskell (その2) Advent Calendar 2017 - Qiita 12/19
ことり、穂乃果と一緒に学ぶHaskell(入門)その6「高階データ型」 - Qiita
+by aiya000 on Haskell (その2) Advent Calendar 2017 - Qiita 12/24
型を実行時に作る:怖くないリフレクション - Qiita
+by mod_poppo on Haskell (その4) Advent Calendar 2017 - Qiita 12/19
Haskellにおける型クラス制約の役割 - Qiita
+by HirotoShioi on Haskell (その5) Advent Calendar 2017 - Qiita 12/04
HaskellのStateの必要性が,プログラミング言語の処理系を書いた時にわかったので,Stateの良さを語ります - ncaq
+by ncaq on Haskell (その3) Advent Calendar 2017 - Qiita 12/01
Haskell - $の仕組みを覗いてみよう - Qiita
+by grainrigi on Haskell (その3) Advent Calendar 2017 - Qiita 12/08
Mapping things
+by b123400 on Haskell (その3) Advent Calendar 2017 - Qiita 12/10
Maybe自作から学ぶHaskell! - Qiita
+by elipmoc101 on Haskell (その3) Advent Calendar 2017 - Qiita 12/16
Listで遊ぶ | 慶應義塾大学ロボット技術研究会
+by mt_caret on Haskell (その4) Advent Calendar 2017 - Qiita 12/20
Arrowを理解する - Qiita
+by Lugendre on Haskell Advent Calendar 2017 - Qiita 12/08
探索問題を Haskell で解く - Qiita
+by ryohji on Haskell (その3) Advent Calendar 2017 - Qiita 12/09
巡回セールスマン問題を遺伝的アルゴリズムとデータ構造を使ってHaskellで解く! - Qiita
+by v97ug on Haskell (その3) Advent Calendar 2017 - Qiita 12/15
いつからHaskellの5行クイックソートが遅いと錯覚していた? - Qiita
+by as_capabl on Haskell (その3) Advent Calendar 2017 - Qiita 12/20
キューの効率的な実装 または私は如何にしてHaskellを止めてF#を愛するようになったか - Qiita
+by rst76 on Haskell (その2) Advent Calendar 2017 - Qiita 12/21
永続リアルタイムキューのHaskell実装と計算量解析 - autotaker’s blog
+by autotaker1984 on Haskell (その2) Advent Calendar 2017 - Qiita 12/22
Zipperに挑む - Qiita
+by Aruneko on Haskell Advent Calendar 2017 - Qiita 12/02
Tree: 親子関係の付け替え - Qiita
+by nobsun on Haskell Advent Calendar 2017 - Qiita 12/07
コラッツの問題をHaskellで書いてみた - Zodiacの黙示録
+by zodi_G12 on IQが1 Advent Calendar 2017 - Adventar 12/06
[Haskell] とびだせ!Hask圏 - Qiita
+by tezca686 on Haskell (その3) Advent Calendar 2017 - Qiita 12/14
区間代数と無限小と無限大 - Qiita
+by makoraru on Haskell (その3) Advent Calendar 2017 - Qiita 12/17
しりとりの圏の回答、または定理証明Haskellを少しだけ - Qiita
+by as_capabl on Haskell (その2) Advent Calendar 2017 - Qiita 12/04
しりとりの圏の実装(未完) - Qiita
+by hiratara on Haskell (その2) Advent Calendar 2017 - Qiita 12/05
量子コンピューターにはモナドがよく似合う - Qiita
+by kyamaz on Haskell (その2) Advent Calendar 2017 - Qiita 12/17
Haskellと層 - Qiita
+by makoraru on Haskell (その2) Advent Calendar 2017 - Qiita 12/20
Haskell上で有限体を使って遊ぶ - Qiita
+by NaOHaq on Haskell (その4) Advent Calendar 2017 - Qiita 12/22
somehow-morphisms on fixed point written in Haskell - Qiita
+by cutsea110 on Haskell Advent Calendar 2017 - Qiita 12/01
量子プログラミングはHaskellで - Qiita
+by kyamaz on 量子コンピュータ Advent Calendar 2017 - Qiita 12/17
HaskellでもGoみたいにシングルバイナリでアプリケーションをデプロイしたい - Qiita
+by t10471 on Haskell (その2) Advent Calendar 2017 - Qiita 12/09
Haskell on Docker で Portable CLI を作ろう - Qiita
+by algas on Haskell Advent Calendar 2017 - Qiita 12/05
Etaで表現されるデータ型としてのJavaクラスとその継承関係 - Qiita
+by aiya000 on Haskell Advent Calendar 2017 - Qiita 12/25
Haskell (Eta) でJavaFXのEDSLを作る雰囲気を醸し出す - Qiita
+by aiya000 on プロ生ちゃん Advent Calendar 2017 - Adventar 12/25
これから Haskell を学ぶ人のための書籍紹介 - Qiita
+by waddlaw on Haskell (その4) Advent Calendar 2017 - Qiita 12/01
2017年に「すごいHaskellたのしく学ぼう」を読む - Qiita
+by Aruneko on Haskell (その4) Advent Calendar 2017 - Qiita 12/13
拙書「Haskell 教養としての関数型プログラミング」の紹介 - Qiita
+by YoshikuniJujo on Haskell (その4) Advent Calendar 2017 - Qiita 12/24
mt_caret.log - 本1 “Haskell Programming from first principles”
+by mt_caret on 本 Advent Calendar 2017 - Adventar 12/01
Haskell で暗号学的ハッシュを扱う (和訳) - Qiita
+by rounddelta on Haskell (その4) Advent Calendar 2017 - Qiita 12/02
Haskell のアプリケーション向けに軽量の Dockerイメージ を作る (和訳) - Qiita
+by rounddelta on Haskell (その4) Advent Calendar 2017 - Qiita 12/03
Haskell 初心者へのアドバイス (和訳) - Qiita
+by rounddelta on Haskell (その4) Advent Calendar 2017 - Qiita 12/04
将来も使えるテストスイート (和訳) - Qiita
+by rounddelta on Haskell (その4) Advent Calendar 2017 - Qiita 12/05
VS Code で Ghcid を使う (和訳) - Qiita
+by rounddelta on Haskell (その4) Advent Calendar 2017 - Qiita 12/06
HLint のルールを理解する (和訳) - Qiita
+by rounddelta on Haskell (その4) Advent Calendar 2017 - Qiita 12/07
SPJ の憂鬱 (和訳) - Qiita
+by rounddelta on Haskell (その4) Advent Calendar 2017 - Qiita 12/17
SPJとHaskellのエコシステム(和訳) - Qiita
+by reotasosan on Haskell (その4) Advent Calendar 2017 - Qiita 12/18
Haskell のパフォーマンスをデバッグする
+by waddlaw on Haskell (その5) Advent Calendar 2017 - Qiita 12/01
こんにちは。Haskell-jpです。
+ちょっと間が空いてしまいましたが、久々の投稿です。Haskell自体の話と関係なくてすみません!
内容としてはタイトルのとおりなのですが、こちらやこちらをはじめとするPull requestにより、当ブログの記事の投稿が簡単になりました!
+具体的には、下記の点を改善しております。
結果、これまでHaskell-jp Blogに投稿する際に問題となっていた、下記の点が解消されました。
+make
などを実行しなければ、markdownで書いた記事がどのようなHTMLに変換されるかわからなかった。make deploy
するまで、記事をmasterブランチにマージしても公開されなかった。以上を踏まえた、Haskell-jp Blogの投稿手順については、READMEをご覧ください。
+基本的に投稿する人は、Markdownで記事を書いて、Pull requestを送るだけです!
それでは、これからもHaskell-jp Blogをよろしくお願いします! hask(_ _)eller +なお、現在Haskell-jp Blogでは、Haskell Advent Calendar 2017(とその2、その3)の記事を特に精力的に募集しています。 +ぜひこの機会にHaskell-jp Blogに記事を投稿してみませんか?
+このエントリーはHaskell Advent Calendar 2017 24日目の記事兼プリキュア Advent Calendar 2017 24日目の記事です。
+毎度の手口ですが、二つのAdvent Calendarに同時に投稿しています。
HaskellとプリキュアのAdvent Calendarということで、去年に引き続き「タイプセーフプリキュア!」について、開発する上で見つかった問題と、その解決方法について紹介します 1。
+なお、「タイプセーフプリキュア!」そのものの日本語の紹介については、私の去年のHaskell Advent Calendarの記事や同じく去年のプリキュア Advent Calendarの記事をご覧ください。
例えば、あなたはたくさんの仲間と、たくさんのサブコマンドがあるCLIアプリを作っていたとします。
+コードの規約上、サブコマンド一つにつき一つのモジュールで、決まった関数(Haskellであれば[String] -> IO ()
みたいな型の関数でしょうか)を定義するものとします。
+そうした場合、必ずどこかのモジュールで、各モジュールで定義したサブコマンドを表す関数を列挙する必要があるでしょう。
+その場合、次のような問題が生じることがあります。
また、DRY原則を徹底するならば「サブコマンドの名前を、サブコマンド自身の定義と列挙しているモジュールとで繰り返さない」というアイディアに基づき、こうした関数の列挙を避ける、という考え方もあるでしょう。
+そのように作ることで、モジュールに関わる情報(どのような定義で、どのように使用されるのか)をなるべくモジュールのファイルのみに集約させることができ、モジュールに関する情報が分散してしまうのを軽減することができます。
つまり、今回実現したいことは、複数のファイルに散らばった特定の関数やデータ型の定義を、自動で一カ所にまとめて再利用する、ということです。
+この記事で何度も使うことになるので「定義を自動でまとめる問題」と呼ぶことにします。
+これをGHCの各種機能を利用して、Haskellで実現させる方法を考えましょう。
こうした処理をHaskell以外のプログラミング言語で行う場合、例えば下記のような機能を使うことになるでしょう。
+参考のために、私がこれまでに出会ったものを紹介します。
前職時代、私は実際にこの「定義を自動でまとめる問題」に出くわしたのですが、Rubyを使っていたため、下記のようにModule#includedという、対象のモジュールをinclude
(モジュールが提供する機能の継承)したときに呼ばれる、特別なメタプログラミング用のメソッドを使って解決しておりました。
module ListedAsSubCommand
+@listed = []
+
+# このモジュールを include するたびに呼ばれるメソッド。
+ # 引数として、include した Class オブジェクト(または Module オブジェクト)を受け取る
+ def included klass
+ # include した Class オブジェクトをリストに追加して記録する
+ @listed.push klass
+ end
+
+class << self
+ attr_reader :listed
+ end
+ end
+
+
+# path/to/commands/foo/sub_command_a.rb
+class SubCommandA
+include ListedAsSubCommand
+
+# SubCommandA の定義 ...
+ end
+
+
+# path/to/commands/bar/sub_command_b.rb
+class SubCommandB
+include ListedAsSubCommand
+
+# SubCommandB の定義 ...
+ end
このように書くことで、ListedAsSubCommand.listed
というプロパティから、ListedAsSubCommand
をinclude
したClass
オブジェクトのリストが取得できます。
+実際に使用するときは、下記のように、対象のクラスが定義されているファイルを含んだディレクトリーからまとめてrequire
した上で、ListedAsSubCommand.listed
にアクセスする事になるでしょう。
# ListedAsSubCommand.includedが実行されるのは対象のクラスが
+# 定義されたときなので、この時点では空のリスト。
+ListedAsSubCommand.listed #=> []
+
+# Dir.glob メソッドで、指定したディレクトリーから
+# 再帰的にファイルを取り出し、require で読み込む。
+Dir.glob('path/to/commands/**/*.rb') do|file|
+require file
+ end
+
+# require されたファイルの中でクラスの定義が実行されるので、
+# 定義したクラスがリストに追加される
+ListedAsSubCommand.listed #=> [SubCommandA, SubCommandB, ...]
Javaで「定義を自動でまとめる問題」を解決する場合も、Rubyと同様に、何らかの形でメタプログラミング用の仕組みを利用することになるかと思います。
+とりわけ、Javaにおいては、この問題の解決に特化しているライブラリーの機能が存在している点が興味深いでしょう。Springの「コンポーネントスキャン」です。
SpringをはじめとするDIフレームワークでは、各クラスにおいて依存するオブジェクト(正確にはそのインターフェース)を宣言した際、必ず何らかの形で、「どのインターフェースにどのオブジェクトを紐付けるか」を宣言することになります。いわゆるApplication Contextを書いたXMLであったり、@Configuration
アノテーションが着いたクラスがそれに当たります。
+結果、モジュール(実際にはJavaなのでクラス)に関する情報、すなわちどのクラスのどのフィールドに、どのオブジェクトを注入するか、といった情報はすべてモジュールのファイルとは独立して管理することになり、DRYではなくなってしまいます。 まさに「定義を自動でまとめる問題」の典型と言えますね。
それに対してSpringの「コンポーネントスキャン」では、下記のように設定することで、「どのインターフェースにどのオブジェクトを紐付けるか」といった情報を、すべて自動で設定してしまうことができます。
+下記はコンポーネントスキャンを@Configuration
アノテーションが着いたJavaのクラスで設定した場合のサンプルコードです。
@Configuration
+@ComponentScan("example.base.package.containing.components")
+public class AppConfig {
+}
@Configuration
アノテーションを付与したJavaのクラスに、更に@ComponentScan
というアノテーションを付与すると、Springは、@ComponentScan
アノテーションの引数として渡した名前空間以下に存在する、すべての@Component
というアノテーションが着いたクラスのオブジェクトを、自動的にほかの@Component
が着いたクラスのフィールドとして設定できるようにします2。
@Component
+public class SomeComponent {
+// ...
+ }
このようにコンポーネントスキャンを使うことで、@ComponentScan
されたクラスのオブジェクトは自動で依存するオブジェクトとして紐付けられるようになります。
+従来foo-context.xml
みたいな名前のファイルに、どのオブジェクトのどのフィールドにどのオブジェクトを紐付けるか、といった情報を一つ一つ書いていたのを、ほとんど書かなくて済むようになりました。
さて、私が経験した二つの言語における「定義を自動でまとめる問題」の解決方法を見てきたところで、この問題を解決するのに共通して必要なことを列挙しましょう。
+(1) 対象となる「まとめたい定義(モジュールや関数、型など)」が書かれているファイルが、どのディレクトリー以下にあるか設定する
+「定義を自動でまとめる問題」に取り組むに当たり、最低限必要となるのが、この設定です。
+まさかファイルシステムにあるすべてのソースコードから「まとめたい定義」を探すわけにも行きませんし、プロジェクトのディレクトリーすべてを処理するのも、柔軟性に欠けた解決方法でしょう。そこで通例「定義を自動でまとめる問題」に対応する際は、「まとめたい定義(モジュールや関数、型など)」が書かれているファイルがどのディレクトリー以下にあるか、を何らかの形で書くことになります。
前述のRubyによる例の場合、この情報は下記のDir.glob
メソッドに渡した引数に当たります。
+'path/to/commands/**/*.rb'
という文字列のうち、 path/to/commands/
の部分ですね。
Dir.glob('path/to/commands/**/*.rb') do|file|
+require file
+ end
JavaにおけるSpringのコンポーネントスキャンの場合、@ComponentScan
アノテーションに渡した引数が該当します。
+厳密には、@ComponentScan
アノテーションに渡す引数はディレクトリーのパスではなくJava
のパッケージの名前ですが、Javaではパッケージはクラスパス以下のディレクトリーと一対一で対応するよう作る必要があるので、事実上ディレクトリーのパスを渡していると言えるでしょう。
@Configuration
+@ComponentScan("example.base.package.containing.components")
+public class AppConfig {
+}
(2) 「まとめたい定義(モジュールや関数、型など)」が書かれたファイルに、なんらかの印をつける
+「定義を自動でまとめる問題」では、「どの定義を自動でまとめるか」さえ指定できればよいので、理屈の上では前述の「(1) 対象となる『まとめたい定義(モジュールや関数、型など)』が書かれているファイルが、どのディレクトリー以下にあるか設定する」さえできれば、後はディレクトリー以下のファイルをすべて自動でまとめられるはずです。 +しかし、それだけでは次の問題が生じてしまう恐れがあります。
+Commands
というディレクトリー以下に複数のサブコマンド(まとめられる対象)を置いたとき、各サブコマンドで共有されるユーティリティー関数もCommands
ディレクトリー以下に置きたくなるかも知れません。もちろん状況に応じてほかのディレクトリーに置く手段も検討すべきですが、そうしたユーティリティー関数の入ったファイルは自動でまとめて欲しくないでしょう。そうした問題を軽減するために、「定義を自動でまとめる問題」に対応する際は、必ず「『まとめたい定義(モジュールや関数、型など)』が書かれたファイルに、なんらかの印をつける」ことを検討した方がいいと思います。
+前述のRubyによる例で言えば、これはinclude ListedAsSubCommand
という、included
メソッドを実装したListedAsSubCommand
モジュールをinclude
することが該当します。
+JavaのSpringのコンポーネントスキャンの場合、まさしく@Component
アノテーションがそれに当たるでしょう。
これらの印が着いたファイルを読む場合、この「印」を手がかりにして、コードベースを検索したり定義ジャンプしたり、Springの場合はインターネットを検索したりすることで、「印」の役割を知り、そのファイルがどう使われるのか調べることができるのです。
+いよいよ次の節で「定義を自動でまとめる問題」をHaskellで解決した例を紹介いたしますが、その前にこの問題を解決することによって生じる、副作用について強調しておきましょう。 +私の観測範囲内でですが、今までこの問題に対応した例を見たことがないのは、そうした副作用による悪影響が大きいと感じている人が多数派だからなのかも知れません。
+それは、前節でも触れましたが、「『自動でまとめられるファイル』がどのように使用されるか理解しにくくなる」ということです。
+この問題は、確かに「『まとめたい定義(モジュールや関数、型など)』が書かれたファイルに、なんらかの印をつける」ことである程度緩和可能な問題ではありますが、それでも強く意識するべきでしょう。
+「自動でまとめられるファイル」を初めて読んだ人が、include ListedAsSubCommand
や@Component
という印に気づければよいのですが、そうでない場合、使用箇所を求めてコードベースをさまようことになってしまいます。
+事前に「印」の存在を知らせておくに越したことはありません。
それから、「『まとめたい定義(モジュールや関数、型など)』が書かれたファイルに、なんらかの印をつける」ことを選択した場合、「まとめたい定義が書かれたファイル」を新しく追加したいとき、ファイルにその「印」を書き忘れてしまうことがある点も、覚えておくべきでしょう。
+当初この「定義を自動でまとめる問題」を提起した際、自動でまとめなかった場合のデメリットしてあげた、
+++
+- サブコマンド(を表す関数)を追加したとき、サブコマンドを列挙しているモジュールに、追加し忘れる。
+
という問題と本質的に同じです。
+自動でまとめずに手で定義を列挙した場合と比べて、編集するファイルが少ない分、忘れる可能性は低いかもしれません。
+ひな形に「印」を含めれば、さらに忘れる確率を下げることができるでしょう。手で一つのファイルに定義を列挙していた場合、そうした工夫はできません。
+ですが、いずれにしても忘れてしまうリスクがあることは変わらないでしょう。
以上の通り、結局のところ、「定義を自動でまとめる」よう設定するか、単純にまとめたい定義を手で列挙するかどうかは、そうしたトレードオフを考慮しつつ落ち着いて考えるのを推奨します。
+これから紹介する方法を採用する際も、ここであげた注意点については忘れないでください。
cure-index.json
の実装「タイプセーフプリキュア!」(パッケージとしての名前はtypesafe-precureなので、以下「typesafe-precure」と呼びます)では、最近の更新により、コンパイル時に「cure-index.json」と、「pretty-cure-index.json」いうファイルを生成するようになりました。
+次のような内容のファイルです。
{
+"specialItems": [
+ {
+ "nameEn": "Sweets Pact",
+ "attachments": [
+ "Animal Sweets"
+ ],
+ "nameJa": "スイーツパクト",
+ "id": "SweetsPact"
+ },
+ ...
+ ],
+ "transformees": [
+ {
+ "nameEn": "Cure Whip",
+ "variationEn": "",
+ "nameJa": "キュアホイップ",
+ "variationJa": "",
+ "id": "CureWhip",
+ "introducesHerselfAs": "ショートケーキ!元気と笑顔を!レッツ・ラ・まぜまぜ!キュアホイップ!できあがり!"
+ },
+ ...
+ ],
+ "girls": [
+ {
+ "nameEn": "Ichika Usami",
+ "nameJa": "宇佐美 いちか",
+ "id": "Ichika"
+ },
+ ...
+ ],
+ ...
+ }
これは、変身アイテムからプリキュア、プリキュアに変身する前の女の子、それから浄化技や変身時の台詞まで、typesafe-precureで定義されているあらゆる情報をまとめたJSONです。
+まさしく、プリキュアの定義を自動でまとめた「インデックス」となっております 3。
+ただし、残念ながら現時点では「キラキラ☆プリキュアアラモード」に収録されたプリキュアの情報しか、cure-index.json
には記録されていません(理由は後で説明します)。
名前の通り、pretty-cure-index.json
にはcure-index.json
をプリティープリントしたJSONが記録されています。
+下記のようにcurl
して確かめてみましょう。
$ curl -sL https://github.com/igrep/typesafe-precure/raw/master/gen/cure-index.json
+{"girls":[{"id":"Ichika","nameEn":"Ichika Usami","nameJa":"宇佐美 いちか"},{"id":"Himari","nameEn":"Himari Arisugawa","nameJa":"有栖川 ひまり"}
+...
+$ curl -sL https://github.com/igrep/typesafe-precure/raw/master/gen/pretty-cure-index.json
+{
+"specialItems": [
+ {
+ "nameEn": "Sweets Pact",
+ ...
さて、このcure-index.json
、繰り返しになりますが、typesafe-precureで定義されている、すべてのプリキュアの情報をまとめたJSONとなっております。
+ライブラリーとしてのtypesafe-precureでは、これらの情報は一つ一つがHaskellの型として定義[^detail-typesafe-precure]されており、cure-index.json
は、それらの情報をコンパイル時に「自動でまとめる」ことで作成されます。決して、JSONからHaskellの型を作っているわけではありません。
+詳細は冒頭にも挙げましたが、私の去年のHaskell Advent Calendarの記事や同年のプリキュア Advent Calendarの記事をご覧ください。
+ここではそれを実現するために使用した、Haskellで「定義を自動でまとめる」方法を紹介しましょう。
…と、その前に、今回typesafe-precureのビルドに使用したGHCのバージョンを述べておきましょう。
+typesafe-precureは現在(ver. 0.5.0.1)の時点において、通常GHC 8.0.2でビルドされています。
+特にCIでの確認はしていませんが、GHC 7.10でもビルドできるはずです。
+従って、使用しているtemplate-haskellパッケージは2.10.0.0から2.11.1.0となっています。
この記事で紹介する機能は、GHC(と、GHCに標準添付されるtemplate-haskellパッケージ)のバージョンによって、大きく変わる場合があります。
+今回は「できない」としたことも、将来のGHCではできるようになっている(あるいは運悪くその逆もある)かもしれません。
+あらかじめご了承ください。
なお、各バージョンのGHCに標準添付されているパッケージのバージョンについては、Commentary/Libraries/VersionHistory – GHCをご覧ください。
+まず、「『まとめたい定義(モジュールや関数、型など)』が書かれたファイルに、なんらかの印をつける」方法を考えましょう。
+実はHaskell(GHC)にもアノテーションがあります(Javaのアノテーションと使い勝手が異なりますが)。
+ANN
というGHCのプラグマ({-# ... #-}
という形式で表される、特別なコメント)を使用すると、下記のように、モジュールや型、名前が付いた値に対して、アノテーションを加えることができます(例はアンッ!!!アンッ!!!! - Qiitaから拝借しました)。
module Foo where
+{-# ANN module ("annotation" :: String) #-} -- モジュールに対する注釈。importの前には書けないっぽい。不便……
+
+data Foo = Foo
+{-# ANN type Foo (2 :: Int) #-} -- 型に対する注釈
+{-# ANN type Foo (5 :: Int) #-} -- 注釈を同じ/違う型で複数個付ける事も出来る
+{-# ANN type Foo (2.4 :: Double) #-}
+
+foo :: Foo
+= Foo
+ foo {-# ANN foo (3 + 2 * 6 :: Int) #-} -- 値に対する注釈。注釈の中で計算する事も可能
上記の通り、GHCのANN
は、Javaのアノテーションと異なり、アノテーション専用のインターフェースを作って引数を補足情報として渡す、というような形式ではありません(そもそもHaskellにはインターフェースなんてありませんしね)。
+Data
型クラスのインスタンスである型の値であれば、なんでもアノテーションとして設定できます。
そのData
型クラスのインスタンスですが、base
パッケージに含まれている多くの型に加え、DeriveDataTypeable
というGHCの言語拡張を使えば、オリジナルの型も簡単にそのインスタンスにすることができます。
{-# LANGUAGE DeriveDataTypeable #-}
+
+import Data.Data
+
+data SomeOriginalType =
+SomeOriginalValue deriving Data
この、Data
型クラスを使えば、実行時に型の構造を取得したりすることができます。
+とはいえ、ここでは単純に{-# LANGUAGE DeriveDataTypeable #-}
とderiving Data
を「おまじない」として使うだけで差し支えありません。
+詳しく知りたい方は「What I Wish I Knew When Learning Haskell 日本語訳」の「ジェネリクス」の章をご覧ください。
さてtypesafe-precureでは、このData
型クラスとANN
プラグマを利用した次のようなアプローチで、各モジュールに対し、プリキュアやプリキュアに関する情報を「印」として付与しました。
ACME.PreCure.Index.Types
というモジュールに、型の名前やインスタンスの定義を自動生成したり、それをJSONに変換したりするのに使う、中間データのための型を作る。
data Girl =
+Girl { girlId :: String, girlNameEn :: String, girlNameJa :: String }
+ deriving (Eq, Show, Data)
Data
型クラスのインスタンスとすることで、「まとめたい定義」が含まれたモジュールに、その中間データ用の値をANN
プラグマで付与できるようにする。名前がACME.PreCure.Textbook.*.Profiles
という形式のモジュール4(「キラキラ☆プリキュアアラモード」での例)で、中間データの値(つまり各プリキュアや変身アイテムなどについての情報)を定義する。
girls :: [Girl]
+=
+ girls "Ichika Usami" "宇佐美 いちか"
+ [ mkGirl "Himari Arisugawa" "有栖川 ひまり"
+ , mkGirl "Aoi Tategami" "立神 あおい"
+ , mkGirl "Yukari Kotozume" "琴爪 ゆかり"
+ , mkGirl "Akira Kenjo" "剣城 あきら"
+ , mkGirl "Ciel Kirahoshi" "キラ星 シエル"
+ , mkGirl ]
1. ACME.PreCure.Textbook.*.Profiles
で定義した中間データを、ACME.PreCure.Textbook.KirakiraALaMode.Types
という形式のモジュールに対してANN
プラグマで付与する(同じく「キラキラ☆プリキュアアラモード」での例)。
module ACME.PreCure.Textbook.KirakiraALaMode.Types where
+
+import ACME.PreCure.Textbook.KirakiraALaMode.Profiles
+
+{-# ANN module girls #-}
先の手順で引用したコードをご覧になった方は、こんなことを疑問に思ったかも知れません。
+中間データの値を定義するモジュールと、ANN
で中間データの値を付与するモジュールとを分ける必要があるのか、と。
+上記の例で言えば、一つのモジュール(ACME.PreCure.Textbook.KirakiraALaMode.Types
)でgirls
を定義しつつANN
で付与すればよいのではないか、ということです。
+あるいはgirls
という名前をつけずに、
{-# ANN module
+ [ mkGirl "Ichika Usami" "宇佐美 いちか"
+ , mkGirl "Himari Arisugawa" "有栖川 ひまり"
+ , mkGirl "Aoi Tategami" "立神 あおい"
+ , mkGirl "Yukari Kotozume" "琴爪 ゆかり"
+ , mkGirl "Akira Kenjo" "剣城 あきら"
+ , mkGirl "Ciel Kirahoshi" "キラ星 シエル"
+ ]
+#-}
+-- 注: このコードは試していないので文法が合っているか自信がないです。
というような書き方はできないのか、ということです。
+中間データの値をANN
で使うだけならそれで問題ないのですが、typesafe-precureの場合、中間データの値からプリキュアや変身アイテムを表す型と、その型クラスのインスタンスを定義する必要があります。
+なので、先ほどのコード例にあったACME.PreCure.Textbook.KirakiraALaMode.Profiles
というモジュールでは、実際には{-# ANN module girls #-}
の行の後に、girls
から、プリキュアに変身する女の子(を表す型)や、それに対して型クラスのインスタンスを宣言するTemplate Haskellのコードが続いています。
+下記の$(declareGirls girls)
という行がそれです。
{-# LANGUAGE TemplateHaskell #-}
+module ACME.PreCure.Textbook.KirakiraALaMode.Types where
+
+import ACME.PreCure.Textbook.KirakiraALaMode.Profiles
+
+{-# ANN module girls #-}
+$(declareGirls girls)
詳細は冒頭でも挙げた私の去年のHaskell Advent Calendarの記事などをご覧いただきたいのですが、typesafe-precureでは、それぞれのプリキュアや、プリキュアに変身する女の子、変身に必要なアイテムなどを、すべて個別の型として定義しています。
+そのため、中間データの値はJSONとしてまとめるだけでなく、個別の型として定義する必要もあったのです。
+その結果、中間データの値は必ず名前をつけて使い回さないといけなくなるのです。
そして、ANN
やTemplate Haskellにおいて値に名前をつけて使い回す場合、「Stage Restriction」というやっかいな制限が顔を出してきます。
+これは、「ANN
で値を付与する式や、トップレベルの宣言などを生成するTemplate Haskellのコードでは、ほかのモジュールからimport
された名前しか参照できない」という制限です(詳しくは「できる!Template Haskell (完)」をご覧ください)。
+これがあるために、中間データの値を含めた名前(上記のコードの場合girl
)は、ANN
やTemplate Haskellで参照するモジュールとは一旦別のモジュールとして定義して、import
して再利用するしかありません。
本来、「定義を自動でまとめる問題」に対応する目的の中には「モジュールに関わる情報(どのような定義で、どのように使用されるのか)をなるべくモジュールのファイルのみに集約させる」というものがありましたが、外部のファイルに書くボイラープレートが増えてしまい、この観点ではイマイチな実装になってしまいました。
+この点については、後の節でよりよい方法を検討しましょう。
前節までで紹介した方法により、ANN
プラグマを使うことでプリキュアの情報が書かれたモジュールに、プリキュアの情報を「自動でまとめる」ための「印」を着けることができました。
+続いて、ANN
プラグマで「印」を着けたモジュールがどこにあるかを指定して、GHCに自動で回収させる方法を述べましょう。
+「解決に必要なもの」の節で説明した、「対象となる『まとめたい定義(モジュールや関数、型など)』が書かれているファイルが、どのディレクトリー以下にあるか設定する」部分に当たります。
次の節で説明しますが、ANN
プラグマで付与した情報は、「アンッ!!!アンッ!!!!」でも説明されているとおりreifyAnnotations
というTemplate Haskellの関数を使えば取得することができますが、該当のモジュールを何らかの方法で集めなくてはなりません。
+私が調べた限り、少なくともTemplate Haskellを使う限りは、import
しているモジュールから収集する方法しか見つかりませんでした。
+Template Haskellのライブラリーのドキュメントでは、reifyAnnotations
するのに必要な、Module
型の値を取得する方法として、thisModule
を使ってTemplate Haskellのコードを実行しているモジュールから取得するか、reifyModule
関数を使ってthisModule
からthisModule
がimport
しているモジュールから取得するしか紹介されていないためです。
しかし、現状のGHCではTemplate Haskellをもってしても、指定したディレクトリー以下のモジュールを自動でimport
するということはできません。
+あまりユーザーに自由を与えてしまうと、却って混乱が生じる恐れがあるので敢えて実装していないのでしょう。
+とは言え、だからといって「印」を着けたモジュールを一つずつ手でimport
して列挙してしまっては、「定義を自動でまとめる問題」を解決できたとは言えなくなってしまいます。
+そこで、今回は実践でもよく使われる、さらなる「裏技」を用いることにしました。
+本節の見出しでネタバレしてしまっていますが、autoexporter
というプログラムと、GHCのカスタムプリプロセッサーのためのオプションを使います。
autoexporterは、ドキュメントに書いてあるとおり、GHCのカスタムプリプロセッサーのためのオプション(-F -pgmF
)、さらにはOPTIONS_GHC
プラグマ組み合わせて、次のように使うことを想定して作られています。
+以下は、typesafe-precureのACME/PreCure/Textbook.hs
というファイルからの抜粋です。
{-# OPTIONS_GHC -F -pgmF autoexporter #-}
と、いっても1行だけですが😅
+一つずつ解説しましょう。
+まずOPTIONS_GHC
プラグマですが、文字通りこれはghc
コマンドに渡すオプションを、ファイル単位で指定するためのものです(もちろんすべてのオプションをファイル単位で指定できるわけではありません)。
+つまり、上記の場合-F -pgmF autoexporter
というオプションが、ACME/PreCure/Textbook.hs
というファイルでのみ有効になります。
続いて-F
オプションですが、これは「カスタムプリプロセッサー」という機能を有効にするためのものです。
+これを有効にすると、有効にしたファイルを、続く-pgmF
オプションで指定したプログラムで変換するようになります。
+具体的には、-pgmF
オプションで指定したプログラムに、
という3つのコマンドライン引数を渡して、-pgmF
オプションで指定したプログラムを実行します。
+-pgmF
で指定したプログラムが、3つめの引数として渡した名前のファイルに変換後のソースコードを書き込むことで、-F
を有効にしたファイルを、変換後のソースコードでそっくりそのまま差し替えます。
+結果、-pgmF
オプションで指定したプログラムは、自由に任意のHaskellのソースを生成できるようになります。まさにソースコードの自動生成にぴったりな機能と言えるでしょう。
ちなみにこの機能、hspec-discover
などのパッケージでも使用されています。テストコードを複数のファイルに分けて書く場合はほぼ必ず使われるものなので、みなさんも「おまじない」として使用したことがあるでしょう(-F -pgmF
なんて文字列、ググラビリティーも低いですしね。)。
+そういえばこれもテストコードの「定義を自動でまとめる問題」を解決したものでしたね!
話がそれましたが、autoexporter
はこのカスタムプリプロセッサーを利用することで、次のようなソースコードを自動生成します。
+autoexporter
のドキュメントにも同じことが書かれていますが、ここでもACME/PreCure/Textbook.hs
を例に説明しましょう。
module ACME.PreCure.Textbook
+module ACME.PreCure.Textbook.First
+ ( module ACME.PreCure.Textbook.MaxHeart
+ , ...
+ module ACME.PreCure.Textbook.KirakiraALaMode
+ , where
+ )
+import ACME.PreCure.Textbook.First
+import ACME.PreCure.Textbook.MaxHeart
+...
+import ACME.PreCure.Textbook.KirakiraALaMode
そう、(プリキュアが好きで)賢明なHaskellerのみなさんならお気づきでしょう。typesafe-precureのACME/PreCure/Textbook/
ディレクトリーに含まれている、(プリキュアの各シリーズを表す)すべてのモジュールをimport
して、再エクスポートしているのです!
つまり、autoexporter
はこのような、「責務を分割するためにモジュールを細かく分けたい、でもユーザーには一つのモジュールをimport
しただけで使えるようにしたい」というライブラリー開発者のニーズに応えるため、よく行われているモジュールの書き方を自動で行うための便利コマンドなのです。
紹介が長くなりましたが、typesafe-precureではこのautoexporter
を次のように使うことで、「まとめたい型(プリキュアや変身アイテムなどの情報)」が書かれているモジュールを集めています。
ACME.PreCure.Textbook
モジュールでautoexporter
を使うことで、ACME.PreCure.Textbook
以下にある、「まとめたい型(プリキュアや変身アイテムなどの情報)」が書かれているモジュールをすべて自動的に再エクスポートする。ACME.PreCure.Index
モジュールがACME.PreCure.Textbook
モジュールをimport
することで、実際にcure-index.json
などの書き出しを行うACME.PreCure.Index
モジュールが、ACME.PreCure.Textbook
が再エクスポートしたすべてのモジュールを利用できるようになる。実際のところOPTIONS_GHC -F
をもっとうまく使えば、ACME.PreCure.Textbook
以下にあるモジュールを自動ですべてimport
するモジュールと、それを利用してcure-index.json
などの書き出しを行うモジュールを、分けずに一つのモジュールで済ますこともできたでしょう。
+今回は敢えてautoexporter
を再利用することで、ACME.PreCure.Textbook
以下にあるモジュールをすべて回収する処理を書かずに任せることにしました。
+この件については後ほど再検討しましょう。
ANN
プラグマで付与した定義情報から、JSONを書き出すいよいよ、autoexporter
を駆使して集めたモジュールから、ANN
で付与したプリキュアの情報を取り出し、JSONに変換して書き出しましょう。
+詳細はACME.PreCure.Index
モジュールや、ACME.PreCure.Index.Lib
モジュールのソースコードをご覧いただきたいのですが、ここでは簡単にアルゴリズムを解説します。
ACME.PreCure.Index
)」を取得する。import
しているモジュールから、ACME.PreCure.Textbook
モジュールを見つけて、取り出す(具体的には38行目から39行目)。ACME.PreCure.Textbook
モジュールがimport
している、プリキュアの情報を集めたモジュール(ANN
プラグマでプリキュアの情報を付与したモジュール)をすべて取り出す(具体的には42行目から45行目)。ANN
プラグマで付与されているプリキュアや変身アイテムなどの情報を集めて、種類ごとに一つのリストとしてまとめる(具体的には48行目から60行目)。Index
という型の値を、それぞれJSONに変換して書き込む(具体的には48行目から60行目)。上記のアルゴリズムにおいても、Template Haskellの「Stage Restriction」と戦わなければならないということは注記しておきましょう。
+つまり、ACME.PreCure.Index
におけるTemplate Haskellのコードで繰り返し使う便利な関数は、ACME.PreCure.Index
とは別のモジュールで定義して、import
して使わなければならないのです。
+ACME.PreCure.Index.Lib
モジュールは、その制限を回避するためのモジュールです。
ともあれこうして、typesafe-precureではACME.PreCure.Index
モジュールをコンパイルする度に、各モジュールに定義されたすべてのプリキュアに関する情報を集めて、genディレクトリーにあるcure-index.json
やpretty-cure-index.json
というファイルに書き出すことができました。
+「定義を自動でまとめる問題」、これにて一件落着です!🎉
+なお、自動生成されるファイルをGitで管理することはなるべく避けた方がよいことですが、cure-index.json
の配布を簡単に行うため方策として用いることにしています。
typesafe-precureにおける「定義を自動でまとめる問題」の解決方法はここまで述べたとおりですが、今後同じような問題に対応したくなったときのために、最初に思いついたけどうまくいかなかった方法や、後で思いついた別の解決方法をこの先の二つの節ででまとめておきます。
+私や読者のみなさんがお仕事など、より重要なプロジェクトでこれらのアイディアを活かすことができれば幸いです。
当初(実は今も大部分は)、typesafe-precureには、ACME.PreCure.Textbook.KirakiraALaMode.Profiles
で定義しているような中間データはなく、各プリキュア(や、変身アイテムなど諸々)に対しては、直接型を宣言したり型クラスのインスタンスを実装したりしていました。
+例えば下記のようなコードです5👇
data CurePeach = CurePeach
+
+data CureStickPeachRod = CureStickPeachRod
+
+instance Purification CurePeach CureStickPeachRod where
+=
+ purificationSpeech _ _ "届け!愛のメロディ!"
+ [ "キュアスティック・ピーチロッド!"
+ , "悪いの悪いの飛んでいけ!"
+ , "プリキュア!ラブサンシャイン・フレッシュ!"
+ , ]
今回作ったcure-index.json
を最初に思いついたとき、「型クラスから各型のインスタンス宣言を自動で収集して、そこからcure-index.json
を作れないだろうか」と、漠然と考えていました。
+typesafe-precureを作り始める以前、私はRubyで「定義を自動でまとめる問題」に対応した際、Rubyでの場合の節で紹介したような方法を用いていたため、「Haskellにおける、Rubyで言うところのmix-inされるモジュールは型クラスだ」なんて類推をしていたからかも知れません。
+いずれにしても、そんな方法で実現できれば、既存のtypesafe-precureのモジュールの構造をそのまま使ってcure-indexが作れるので、大変都合がよかったのです。
しかし、残念ながらその方法は、少なくとも単純にTemplate Haskellを使うだけでは不可能であるとすぐ気づきました。
+なぜなら、Template Haskellのライブラリーが提供するreifyInstances
という関数は、インスタンス宣言を取り出したい型を、自前で持ってきて引数として渡さなければならないからです。
+したがって、Rubyでやっていたように、型クラスのインスタンスを自動でリストアップする、といったことはできません(もちろん、Rubyでやった時も完全に自動ではなく、include
したクラスが自分でグローバルなリストに追加していたわけですが)。
+それならば、自前でimport
しているモジュールから定義されている型を収集することはできないだろうか、と思って、指定したモジュールで定義されている型を取り出すAPIを探ってみましたが、それも見つかりませんでした。
+最もそれらしいことができそうなreifyModule
という関数が返すModuleInfo
も、保持しているのはあくまでもimport
している別のモジュールだけであり、いくらreifyしてもモジュールの中で定義されている型の情報はとれないのです。
やむなく、私はtypesafe-precureの構造を改め、現在のような、JSONとして書き出すデータ構造を元に型と型クラスのインスタンスを自動で定義するような実装にすることとしました。
+この変更は依然として続いています。具体的には、今年新しく追加された「キラキラ☆プリキュアアラモード」に登場するプリキュア以外は、まだ従来の構造のままで、中間データの値は定義されていません。
+「キラキラ☆プリキュアアラモード」に収録されたプリキュアの情報しか、cure-index.json
に記録されていないのはそのためです。
来年のプリキュアハッカソンやプリキュアAdvent Calendarでは、haskell-src-extsという、HaskellでHaskellのソースコードをパースするライブラリーを使って、この大きな移行プロジェクトに取り組むことになるかと思います。
+typesafe-precureには技術的なネタが尽きませんね。
同じことを繰り返しますが、これから紹介する方法も含めて「定義を自動でまとめる」問題の解決は、どんな方法を使うにしても、多かれ少なかれ凝ったメタプログラミングのテクニックを使わなければならなくなります。
+注意点の節で強調したとおり、そのコードベースを初めて読んだ人が迷子にならないよう配慮することは忘れないでください。
その方法は、先の節でも紹介したhspec-discover
でも実際に行われている方法です。
+hspec-discover
は、GHCのカスタムプリプロセッサーを利用して実行することで、テストが書かれたディレクトリーからSpec
という名前で終わるすべてのテスト用モジュールを自動でまとめて、それらをすべて実行するSpec.hs
を、自動で生成します。
+hspec-discover
の場合、ANN
のようなアノテーションは一切使用せず、モジュールの名前やモジュールがエクスポートする名前に規約を設けることで「定義をまとめる対象」を検出しています。
+このように、ANN
のような特別な「印」を着けずに純粋に名前だけで「定義をまとめる対象」を決めることもできます。
+実績もあり、同じような方法をとることは非常に簡単そうです。
しかし、個人的には注意点の節でも述べたとおり、「定義をまとめる」対象であることを表す「印」は、「定義をまとめる」対象のファイルの中にあった方が、わかりやすくていいと思います。
+確かにhspec-discover
のように、公開されていて広く使用されているものであれば、使用したプロジェクトのコードを初めて読む人でも、すぐに理解できる場合が多いでしょう。「何がまとめられるのか」も比較的直感的ですしね。
+とはいえ、私が想定している、例えばアプリケーションのプラグインみたいな、もう少しローカルなコードベースである場合、「印」はより「印」らしいものであった方が、手がかりとして気づきやすいのではないかと思います。
😕初めて「まとめられる」コードを含むファイルを目にして、どのように使用されるのか分からず戸惑う
+⬇️
+🤔{-# ANN MarkedAsFoo #-}
という見慣れないコメントを見つけて、それでコードベースを検索してみる(プラグマは多くのsyntax highlighterで普通のコメントより目立って見えるはずです)
+⬇️
+💡MarkedAsFoo
が着いたモジュールを実際に収集してまとめているコードを見つけて、理解する
という流れで「定義を自動でまとめる」機構の存在に気づくのではないでしょうか。
+あるいはいっそANN
も使わずに、こんな内容のhuman-readableなコメントを「印」とするのもよいかも知れません。
+プログラムで検出するのもそう難しくはないでしょう。
module Foo.Commands.SampleCommand where
+
+-- | このコメントが付いたモジュールの 'execute' という関数は、
+-- Template Haskellによって、自動的に再利用できるよう収集される。
+-- 詳しくは 'Foo.Commands` を読まれたし。
これなら、Foo.Commands
モジュールにヒントがあることが、すぐに分かります。
+hspec-discover
のように、Template Haskellを使わず直接ファイルシステムにあるファイルを開く方法とも、相性がいいはずです。
ほかにもいろいろな方法を考えましたが、これ以上に有効でもなさそうだし、そろそろ時間もなくなってきたので、この辺でまとめたいと思います。
+ANN
プラグマや、GHCのカスタムプリプロセッサー(-F -pgmF
)を組み合わせて使うことによって解決できるが、実際にはGHCのカスタムプリプロセッサーのみで十分可能
+それでは2018年もTemplate HaskellとプリキュアでHappy Hacking!! ❤️❤️❤️
+(記事中で直接リンクを張っていないもののみ)
+実際には「タイプセーフプリキュアそのものを開発する上で見つかった問題」というよりタイプセーフプリキュアの開発をすることで問題解決の実験をしている、といった方が正しいのは内緒。↩︎
もう少し正確に言うと、自動的に設定したいフィールド(あるいはコンストラクターの引数)に@Autowired
というアノテーションが必要ですが、今回の話では本質的ではないので割愛しています。↩︎
もちろん、数年前流行ったあのライトノベルのパロディーではありません。↩︎
誰にも聞かれてはいませんが勝手にお話ししますと、ACME.PreCure.Textbook
という名前は、プリキュアの教科書から来ています。↩︎
現在もそうですが、実際にはTemplate Haskellで定義されているので、typesafe-precureのリポジトリーにはこれと全く同じコードはありません。↩︎
この記事は、Haskell (その4) Advent Calendar 201714日目の記事です。
+枠が空いていたので埋めるために登録しました。
+長くかかった割には実験自体は失敗気味な、昨日のこちらの記事よりは有用な情報じゃないかと思います。
+ほかの言語でもありそうな話ですしね。
すごく簡潔にまとめるとこの間の下記のツイートに収まるのですが、もう少し丁寧に補足するために書きます。
+++ ++学んだことをまとめると
+— Yuji Yamamoto: 山本悠滋 (@igrep) 2017年12月5日 +
- Invalid characterと言われたらchcp 65001しよう
- Permission Deniedと言われたらビルドし直そう
- 日本語のパスが混ざらないよう気をつけよう
- Cのライブラリーはものによる
ですか。多分 #haskell 以外でも有益な話。 +
恐らく一番高確率で遭遇する & 知らないと回避できないのがこれ。
+あ、ほらまたhakyllでビルドしたら起きた!
> stack exec -- site rebuild
+...
+ [ERROR] preprocessed-site\posts/2017/01-first.md: hGetContents: invalid argument (invalid byte sequence)
+GHCがファイルを読み書きする時に使うHandle
というオブジェクトには、文字コードの情報が含まれています。
これはRubyのIO
やPerlのファイルハンドラーにあるような仕組みと大体似ていて、Handle
といったデータの「入り口」を表すオブジェクトに文字コードを紐付けることで、外から入ってくる文字列の文字コードを確実に内部の統一された文字コードに変換してくれます。
+HaskellのChar
型の場合はUTF-32(この場合その言い方でよかったっけ?)のはずです。
このHandle
に紐付ける文字コード、当然のごとくデフォルトではOSのロケール設定に従って設定されるようになってまして、日本語版のWindowsではそう、Windows-31J(またの名をCP932)ですね。
+でも今はもうすぐ2018年。あなたが「メモ帳」でプログラムを書く人でもない限り、新しく作るファイルの大半はUTF-8でしょう。
+UTF-8とWindows-31Jは全然違う体系の文字コードなので、UTF-8なファイルをWindows-31Jのファイルとして読もうとしてもうまくいかないわけです。
+冒頭にあげたinvalid byte sequence
というエラーはまさにそうした場合に起こるエラーです。
+ファイルの読み書きだけでなく標準入出力でもしばしば発生するので覚えておいてください。
多くの場合、このエラーは以下のコマンドをあらかじめ実行しておけば回避できます。
+> chcp 65001
+> stack exec -- site rebuild
+... 動くはず!
+これは、現在開いているコマンドプロンプトで一時的に文字コードを切り替えるコマンドです。
+65001
という数字がUTF-8を指しているようです。
+もとに戻したい場合はchcp 932
と実行しましょう。
> chcp 932
+どうやら「CP932」の「932」はここで出てくる「932」と同じものを指しているようですね!
+どういう仕様なのか分かりませんが、このコマンド、MSYS2のbashでも使用できます。
+ただしchcp
コマンドはC:\Windows\System32\
という、MSYS2ユーザーにとってはあまりPATH
に入れたくない場所に入っています。
+このディレクトリーには、find.exe
など、Unixな方が好んで使うコマンドと同じ名前の非互換なコマンドがゴロゴロ転がっているのです!
なので私はMSYS2を使う時はC:\Windows\System32\
はPATH
から抜いています。
+私と同じような方は下記のようにフルパスで実行しましょう。
/c/Windows/System32/chcp.com 932
+残念ながら、chcp 65001
してもこのエラーが消えないことはあります1。
+私の推測なんですが、どうもchcp 65001
はchcp 65001
したコマンドプロンプト(とかbash)の孫プロセス(つまり、あなたが入力したコマンドの子プロセス)には届かないことがあるようです。
そんなときは、実際にエラーが起きているコマンドの開発元にバグ報告するか、自分で直してみましょう。
+バグ報告する場合は、「chcp 932
してから実行してみて」とお願いすると、バグ報告を受けた開発者も再現しやすくて助かるかも知れません(残念ながら私はやったことがありません)。
+自分で直す場合、いろいろ方法はありますが、対象のHandle
オブジェクトの文字コードを変えることで対処するのが、一番直接的で確実でしょう。
この問題はHandle
に設定された文字コードと実際にやりとりされる文字列の文字コードに食い違いが発生しているため起こるものなのですから、適切な文字コードに変えてしまえばいいのです。
+状況にもよりますがエラーが起きたHandle
が普通のUTF-8なファイルを読み書きするものである場合、下記のようにすれば、問題は回避できるはずです。
import System.IO (hSetEncoding)
+import GHC.IO.Encoding (utf8)
+
+ hSetEncoding handle utf8
それから、実際に私がhaddockのバグを直した時を例に標準出力(または標準エラー出力)でこのエラーが発生した時の対応も紹介しておきます。
+コードだけ貼り付けると、下記のようにすれば少なくともエラーが起こらないようにすることはできます(このコミットとほぼ同じ内容です)。
{-# LANGUAGE CPP #-}
+
+import System.IO (hSetEncoding, stdout)
+
+#if defined(mingw32_HOST_OS)
+import GHC.IO.Encoding.CodePage (mkLocaleEncoding)
+import GHC.IO.Encoding.Failure (CodingFailureMode(TransliterateCodingFailure))
+#endif
+
+...
+
+#if defined(mingw32_HOST_OS)
+$ hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure
+ liftIO #endif
Windowsでしか使用できないモジュールをimport
している関係上、CPPのマクロが混ざって読みにくいですが、重要な部分だけ切り出すと、
hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure
+とすればよいのです。
+一つ一つ解説しましょう。
+まずhSetEncoding
は先ほども触れたとおり指定したHandle
の文字コードを変更する関数です。
+そしてstdout
は名前の通り標準出力を表すHandle
です。
+最後のmkLocaleEncoding TransliterateCodingFailure
ですが、これはWindowsで設定された文字コード(chcp
された文字コードと同じ)を作って、「もし(Unicodeから、あるいはUnicodeに)変換できない文字があった場合、エラーにせず、それっぽい文字に変換する」という設定で返す、という意味です。
結果、chcp 932
な状態でGHCのエラーメッセージにも使われる
↓この文字
+• No instance for (Transformation Nagisa CardCommune_Mepple)
+↑
+が、
+? No instance for (Transformation Nagisa CardCommune_Mepple)
+のように、クエスチョンマークに変換されるようになります。そう、日本語のWindowsでGHCをお使いの方は一度は目にした「?」ではないでしょうか😅
+つまりGHCはデフォルトでhSetEncoding stderr $ mkLocaleEncoding TransliterateCodingFailure
しているものと推測されます。
+いずれにせよ、エラーでプログラムが異常終了しないだけマシですね。
更に補足すると、GHCの文字コードについてより詳しい情報は、GHC.IO.Encodingのドキュメントをご覧ください。
+雑なまとめと言いつつ最初の一つ目が長くなってしまいましたが、ここからは簡単に言います。
+Windowsでstack build
なりghc
なりelm-make
なりとにかくいろいろ動かしていると、「Permission Denied」と言ったエラー(あるいはこれと似たようなメッセージのエラー)に出遭います。
+正直に言って私は原因はサッパリ分かってないのですが、このエラーは大抵の場合何度も同じコマンドを実行すれば再現しませんでした。
+一度や二度ではめげず、繰り返すのがポイントです 😅
+問題が起きているディレクトリーをウィルス対策ソフトのスキャン対象から外してみるとか、Dropboxの同期を一時的に止めてみる、といったこともやってみるといいかもしれません。
あ、あと、「Directory not empty」みたいなのもあったかな。これは同類のはずです。
+Pure Haskellなライブラリーであれば大体OKなんですが、残念ながらCのライブラリー(lib***
みたいな名前でよくOSのパッケージマネージャーに登録されているやつですね)に依存したライブラリーは、Windowsでインストールするのは結構トラブることが多いです。
+まぁ、これはHaskellに限った話ではないでしょう。
対応方法は私が知る限り完全にケースバイケースなので、ここでは知っている対応例をいくつか挙げておきましょう。
+以上です!
+それでは2018年もHaskell on Windows 10でHappy Hacking!! WSLなんて知らないぜ!🏁🏁🏁
Haskell1では各処理系で言語拡張を提供し,LANGUAGE
プラグマというものを利用することで,言語拡張を利用することが許容されています.Haskellのデファクト標準的な処理系GHCも多くの言語拡張を提供しており,その拡張はGHC拡張と呼ばれています.
今回は,このGHC拡張の簡単な紹介と,個人的に良く使う拡張についての簡単な紹介を,全3回に分けて行いたいと思います.対象としては,GHCでHaskellプログラミングをしたことがあり,通常のHaskellの構文や動作方法が分かっている人を考えています.また,この記事はあくまで簡単な紹介に留めるもので,付随する留意点や詳細な機能説明は,大事な箇所は漏らさないよう注意するつもりですが,全てを網羅するつもりはありませんのでその点は注意してください.もし,実際にGHC拡張を使用する際は,GHCのユーザーガイドをよく読んでから使用するのが良いでしょう.
+Haskellには,言語拡張を取り込む方法が標準で提供されています.Haskell標準では,コンパイラプラグマというものが策定されており,これを通してコンパイラに追加情報を提供することができます.コンパイラプラグマは{-#
と#-}
で囲まれ,字句的にはコメントとして扱われます.標準では,インラインプラグマや特殊化プラグマの他に,LANGUAGE
プラグマというものが策定されており,このプラグマを通して言語拡張を指定することができます.
例えば,実装によってCPP
とScopedTypeVariables
という名前の言語拡張が提供されており,それを使いたい場合,次のような文をモジュールの開始前に指定することで,言語拡張が有効になります.
{-# LANGUAGE CPP, ScopedTypeVariables #-}
+
+module A where
また,LANGUAGE
プラグマを複数指定することもできます.
{-# LANGUAGE CPP #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module A where
この機能を通して,多くのHaskell処理系では言語拡張を提供しています.
+Haskellのデファクト標準な処理系GHCも,多数の拡張を提供しており,この拡張がGHC拡張と呼ばれるものです.GHC拡張は,バージョン8.4.2現在,以下の数が提供されています2.
+$ ghc --supported-extensions | wc -l
+235
--supported-extensions
オプションは,現在のGHCで使用できるGHC拡張を表示してくれるオプションです.ただ,GHC拡張は全てが独立した拡張ではなく,互いに依存しあった拡張が多く存在します.また,先頭にNo
がついている拡張は,そのGHC拡張を無効にするような拡張になっています 3 4(例えば,NoImplicitPrelude
拡張はImplicitPrelude
拡張を無効にする拡張です).
また,デフォルトで有効になっている拡張などもあります.例えば,ImplicitPrelude
という拡張はデフォルトで有効になります.現在デフォルトのHaskell 2010をベースにしたモードでGHC 8.4.2を使用する場合,以下の拡張がデフォルトで有効になります 5 6 7.
NondecreasingIndentation
: Haskellのレイアウトルールを変更する拡張です.この拡張を有効にすると,ネストされたdo
式の場合,インデントをしなくていいようになります.ImplicitPrelude
: 暗黙的にPrelude
モジュールがインポートされるようになる拡張です.MonomorphismRestriction
: 単相性制限を課すようにする拡張です.この制限により,関数束縛でなく型注釈もない束縛変数の型は,デフォルティングルールによって単相化されます.TraditionalRecordSyntax
: レコード構文を有効にする拡張です.この拡張では,名前付きのフィールドを持つデータ型を定義し,それを使用することが可能になります.EmptyDataDecls
: コンストラクタを持たないデータ型の定義を許容する拡張です.ForeignFunctionInterface
: FFIが使えるようになる拡張です.この拡張により,foreign import
構文を使用することで,HaskellからCの関数を読み込むことができるようになります.PatternGuards
: case
式において,通常のパターンに加えて,<-
を使用してガードの中でさらにマッチした条件下でパターンマッチができるようになる拡張です.例えば,case (x, y) of { (True, y) | False <- y -> True; _ -> False }
というような式が書けるようになります.DoAndIfThenElse
: if
式の構文を,then
とelse
の前に;
を許容するよう変更する拡張です.これにより,do
式においてthen
やelse
をインデントする必要がなくなります.歴史的経緯で生まれ,互換性のために残されているものの,現状使用が推奨されていない拡張もあります.他に実験的な拡張やかなり大胆な拡張も存在するため,GHC拡張を使用する際はGHCのユーザーガイドをよく読んでから使用するのが良いでしょう.
+GHCでGHC拡張を使用する方法は,Haskell標準のLANGUAGE
プラグマを使用する他に,幾つかあります.まず,GHCにオプションを渡して有効にする方法です.例えば,NoImplicitPrelude
拡張とStrict
拡張を有効にした状態でMain.hs
をコンパイルしたい場合,次のように書けます.
ghc -XNoImplicitPrelude -XStrict --make Main.hs
GHCでは-X
の後に拡張名を続けることで,言語拡張を有効にしてコンパイルすることができます.通常は,LANGUAGE
プラグマを使用するのが良いですが,何らかの事情でLANGUAGE
プラグマを使用できない場合や,デフォルトで有効にしたい言語拡張がある場合などに便利でしょう.特にGHCiで言語拡張を有効にしたくなった場合,このオプションをset
コマンドで指定すると良いでしょう.
>>> :set -XNoImplicitPrelude -XStrict
他にGHC拡張を有効にする方法として,Cabal
の機能を活用する方法があります.cabal
ファイルのビルド情報欄には,default-extensions
というフィールドを指定することができ,そこにデフォルトで有効にしたい言語拡張のリストを書くことで,その拡張を有効にした状態でCabal
がビルドを行ってくれます.例えば,NoImplicitPrelude
拡張とStrict
拡張をデフォルトで有効にしてビルドしたい場合,次のように書きます.
name: TestPackage
+version: 0.0
+synopsis: Small package with a program
+author: Angela Author
+license: BSD3
+build-type: Simple
+cabal-version: >= 1.2
+
+executable program1
+ build-depends: base
+ main-is: Main.hs
+ default-extensions: NoImplicitPrelude, Strict
+以下では,個人的にデフォルトで有効化して使っている拡張を幾つか紹介します.なお,GHCのバージョンは8.4.2でHaskell2010モードで使用することを前提にしています.
+この節では,以下の拡張を紹介します.
+NoImplicitPrelude
: ユーザーガイド - NoImplicitPrelude拡張Haskellでは,Preludeモジュールが暗黙的にimportされます.つまり,Haskellプログラムは暗黙に
+import Prelude
と書いてあると,解釈されるということです.Preludeモジュールには,Int
/IO
といった基本的なデータ型や,Eq
/Functor
といった基本的な型クラス,zip
/putStrLn
といった基本的な関数が含まれています.
Preludeモジュールの暗黙的なimportは,Haskellプログラムを簡潔に書く上では便利ですが,これを無効にしたい場合もあります.
+といった場合です.NoImplicitPrelude
拡張はまさしくこのような場合に,Preludeモジュールを暗黙的にimportしないようにするGHC拡張です.1番目の理由の場合,この拡張をデフォルトで入れずモジュール度に指定すればいいと思いますが,私的には2番目の理由でこの拡張を使うためデフォルトで有効にしています.代替となるpreludeパッケージは幾つか存在しますが,主に
などがあります8.これらのパッケージを探すにはHackageのPreludeカテゴリを参照するといいでしょう.
+私の場合,classy-preludeを使っていますが,それも生で使用しているわけではなく,パッケージごとにpreludeモジュールを作って使用しています.Preludeは,最もよく使うものが提供されているモジュールですから,APIの変更の影響を最も強く受けます.それを外部パッケージに依存させると,パッケージ保守が結構大変です.もし,パッケージごとにpreludeモジュールを作っておけば,パッケージ側やGHCのバージョン変更の影響などでAPIが変更されても,そのモジュール内でフォールバックを設定することで他のモジュールに変更を持ち越す必要がなくなります.これをNoImplicitPrelude
拡張と組み合わせ,
{-# LANGUAGE NoImplicitPrelude #-}
+
+module A where
+
+import MyPrelude
+
+...
と書くことで,保守がかなりしやすくなります.
+この節では,以下の3つの拡張を紹介します.
+BinaryLiterals
: ユーザーガイド - BinaryLiterals拡張NagativeLiterals
: ユーザーガイド - NagativeLiterals拡張HexFloatLiterals
: ユーザーガイド - HexFloatLiterals拡張Haskellには幾つかのリテラルが存在します.例えば,'c'
は文字cを表すChar型のリテラルです.100
は整数100を表すNum a => a
型のリテラルで,100.1
は浮動小数点数100.1を表すFractional a => a
型のリテラルになります.Haskell標準には他にも幾つかリテラルが存在しますが,特に数値は非常に多様な使われ方がなされるため,他の多くの言語はより強力なリテラル表現を持つことがあります.GHC拡張ではこの背景を元に,リテラルに対する幾つかの拡張を提供しています.BinaryLiterals
はNum a => a
型のリテラルに対して,HexFloatLiterals
はFractional a => a
型のリテラルに対して,NegativeLiterals
はどちらに対してもの拡張を,それぞれ提供します.
数値型に対するリテラルは,既存のものでも数種類存在します.通常の数値表現20
,オクテット(8進数)表現0o24
,ヘックス(16進数)表現0x14
の3つです.BinaryLiterals
拡張は,これに加え0b
を接頭辞に付けることでバイナリ(2進数)表現0b10100
を可能にする拡張です.
これらのオクテット表現やヘックス,バイナリ表現は浮動小数点数の表現はできません.しかし,浮動小数点数は実際にはIEEEの規格に則ったデータ表現になりますから,10進数表現よりも16進数表現の方が実態として分かりやすい場合があります.このためHexFloatLiterals
拡張では,接頭に0x
の付くヘックス表現でも浮動小数点数のリテラルを記述できるようにしています.この拡張によって,0.25
は0x0.4
と表記できるようになります.また,指数表記も10進方式のものではなく,ビット方式のものになります.指数表記にはe
ではなくp
を使い,何ビット移動させるか(つまり,2の何乗を掛けるか)を書くようにします.例えば,1.0
は0x0.4p2
と表記できます.また,0.125
は0x0.4p-1
と表記できます.
さて,Haskellには唯一の単項演算子-
があります.この演算子を使用することでnegate 1
の代わりに-1
という表記が可能になります.しかし,この演算子の結合度は非常に弱く,また二項演算子の-
も存在することからf -1
という表記は(f) - (1)
というように解釈されてしまうなどの問題があり,非常に使い勝手が悪い演算子となっていました.また,Haskellの仕様上,-128
という表現は最終的にnegate (fromInteger 128)
という式に脱糖されますが,例えばInt8
などの,負数は-128
まで扱えるが正数は+127
までしか扱えないといったデータ型の場合に,この式はfromInteger
で一度+128
の値になってしまいオーバーフローを起こしてしまうという問題がありました.これを解決するため導入されたのがNagativeLiterals
拡張です.この拡張を導入することで空白を挟まない-1.0
などは1つのリテラルと解釈されるようになります.この拡張を導入後は,次のようになります.
>>> max -1 2 == max (-1) 2 -- before: max -1 2 == max - (1 2)
+True
+>>> data SamplePZ = SamplePZ deriving (Eq, Show)
+>>> instance Num SamplePZ where { fromInteger i | i <= 0 = SamplePZ }
+>>> -100 :: SamplePZ -- before: raise error
+SamplePZ
+>>> - 100 :: SamplePZ
+*** Exception: ...
+>>> instance Fractional SamplePZ where { fromRational r | r <= 0 = SamplePZ }
+>>> -100.10 :: SamplePZ -- before: raise error
+SamplePZ
+>>> - 100.10 :: SamplePZ
+*** Exception: ...
この節では,以下の2つの拡張を紹介します.
+EmptyCase
: ユーザーガイド - EmptyCase拡張EmptyDataDeriving
: ユーザーガイド - EmptyDataDeriving拡張Haskellでは,コンストラクタを一切持たない型を定義できます.これは空のデータ型と呼ばれ,次のように書けます.
+data Empty
このような型はbase
パッケージのData.Void
モジュールでも提供されており,有用な場合があります.しかし,Haskell標準ではこのようなデータ型に対するサポートが薄く,使用する上で不便な場面があります.このサポートを強化する拡張が,EmptyCase
拡張とEmptyDataDeriving
拡張です.
EmptyCase
拡張は,空のパターンマッチを書けるようにする拡張です.Haskell標準では,空のパターンマッチは書けません.つまり,case x of {}
というような式が書けないということです.通常はデータ型は何らかのコンストラクタを持っていますから,このようなパターンマッチを書きたいと思う場面はないでしょう.しかし,空のデータ型においてこのようなパターンマッチを書きたいと思うことがあります.
f :: Empty -> a
+= case x of {} f x
このような表記を可能にするのがEmptyCase
拡張です.なお,このケース式は次のように書くのと同値になります.
f :: Empty -> a
+= x `seq` error "Non-exhaustive patterns in case" f x
もう1つのEmptyDataDeriving
拡張は,空のデータ型に対してderiving
構文を使用できるようにする拡張です.空のデータ型は,通常のデータ型と違いEq
やShow
などの型クラスインスタンスをderiving
することができません.つまり以下のようなことができません.
data Empty
+deriving (Eq, Ord, Show)
しかし,これでは不便な場合があります.それを可能にするのがEmptyDataDeriving
拡張です.この拡張では,Eq
/Ord
/Show
/Read
の4つがderiving
可能になり,それぞれは次のようなインスタンスを生成します.
instance Eq Empty where
+== _ = True
+ _
+instance Ord Empty where
+compare _ _ = EQ
+
+instance Read Empty where
+= pfail
+ readPrec
+instance Show Empty where
+showsPrec _ x = case x of {}
この節では,以下の3つの拡張を紹介します.
+TupleSections
: ユーザーガイド - TupleSections拡張MultiWayIf
: ユーザーガイド - MultiWayIf拡張LambdaCase
: ユーザーガイド - LambdaCase拡張Haskellでは,タプルやラムダ抽象,セクション,if
式やcase
式といった構文が導入されていますが,これらを組み合わせて多用する場合,幾つか冗長な表現が生まれる場合があります.その中でも頻出する表現に対して,新たな構文を提供するGHC拡張があります.それが,TupleSections
,MultiWayIf
,LambdaCase
の3つの拡張です.
Haskellには,セクションと呼ばれる二項演算子の部分適用を表す構文があります.また,Haskellではタプルにも独自の構文が充てがわれています.このタプルを使用する際,セクションのように部分適用を簡潔に書きたい場合があります.例えば,\x -> (1, x)
という表現をもっと簡潔に書きたい場合があります.この場合は(,) 1
というな表記が可能ですが,2番目に部分適用したい場合や,3つ組のタプルに部分適用したい場合などは非常に面倒です.このため,TupleSections
拡張は(1, )
という表記でタプルの部分適用を書ける構文を提供します.2つ以上空きがある場合は,左から引数を受け取っていくようになります.例えば,(True, , "str", )
は\x y -> (True, x, "str", y)
と同等です.
MultiWayIf
は名前の通り複数の条件をガード構文のように指定できるif
式を提供する拡張です.つまり,以下のようなことがかけます.
f :: [Int] -> IO ()
+= sequence_ $ do
+ f xs <- xs
+ x pure $ if
+ | x <= 0 -> fail "non-positive number"
+ | x `mod` 15 == 0 -> putStrLn "FizzBuzz"
+ | x `mod` 3 == 0 -> putStrLn "Fizz"
+ | x `mod` 5 == 0 -> putStrLn "Buzz"
+ | otherwise -> print x
このMultiWayIf
は次のようにcase
式で書き換えることが可能です.
f :: [Int] -> IO ()
+= sequence_ $ do
+ f xs <- xs
+ x pure $ case () of
+ | x <= 0 -> fail "non-positive number"
+ _ | x `mod` 15 == 0 -> putStrLn "FizzBuzz"
+ _ | x `mod` 3 == 0 -> putStrLn "Fizz"
+ _ | x `mod` 5 == 0 -> putStrLn "Buzz"
+ _ | otherwise -> print x _
3つ目のLambdaCase
拡張は,ラムダ抽象とcase
式を組み合わせた際に良く使う表現をより簡潔に書けるようにする拡張です.この拡張を使うと,\x -> case x of (a, b) -> a + b
というようなラムダ抽象を,\case (a, b) -> a + b
と書けるようになります.もちろんレイアウトルールもcase-of
式と同じように作用するため,改行を含んだ式も書けます.
f :: Maybe Int -> Int
+= negate . \case
+ f Nothing -> 0
+ Just x -> x
この節では,以下の3つの拡張を紹介します.
+BangPatterns
: ユーザーガイド - BangPatterns拡張StrictData
: ユーザーガイド - StrictData拡張Strict
: ユーザーガイド - Strict拡張Haskellはデフォルトの評価戦略として,グラフ簡約の遅延評価を採用しています.これはリストや再帰に関する表現を非常に豊かにする反面,パフォーマンスを悪化させたりデバッグを困難にさせる場面が多いなどの負の面もあります.このためHaskell標準では,seq
関数や正格フラグといった正格評価へのサポートも提供しています.しかし,このサポートは表現が冗長な場合が多く,使い勝手が悪い側面があります.この面を解決するための拡張が,BangPatterns
,StrictData
,Strict
の3つの拡張です.
再帰関数において,累積引数は多くの場合正格に計算した方が効率が良いですが,Haskell標準では以下のように書く必要がありました.
+sum :: [Int] -> Int -> Int
+sum xs y = y `seq` case xs of
+:xs' -> sum xs' (x + y)
+ x-> y []
このようなseq
による評価をより簡潔に書けるよう,BangPatterns
拡張というものが提供されています.これはパターンを拡張し,バンパターンというものを導入します.このバンパターンは,通常のパターンに!
を付けることで書けます.例えば,上の例はバンパターンを使うと以下のように書けます.
sum :: [Int] -> Int -> Int
+sum xs !y = case xs of
+:xs' -> sum xs' (x + y)
+ x-> y []
バンパターンはパターンの1つですから,もちろんlet
式やcase
式でもlet !y = f x in y
やcase f x of !y -> y
というように使えます.また,case x of (!y, z) -> y + z
というように部分パターンとしても有効です.バンパターンはHaskellのcase
式の翻訳ルールに次の規則を加えることで実現されます.
case v of { !pat -> e; _ -> e' }
+`seq` case v of { pat -> e; _ -> e' } ≡ v
Haskell標準では,データ型の宣言において,コンストラクタの引数に正格フラグというものを付けることが許容されています.このフラグをつけた引数は,正格に評価された後コンストラクタに渡されます.ただ,一般にデータ型の引数は正格な方が効率が良いため,データ型宣言時に正格フラグを付けるという慣習がありました.この慣習を打破するために導入されたのが,StrictData
拡張です.StrictData
拡張下のモジュールでは,データ型宣言時,コンストラクタの引数は全て正格フラグをつけているものとして扱われます.また,~
というフラグが新たに導入され,このフラグをつけた引数の場合はHaskell標準化のデフォルトの動作,つまり引数は正格に評価されず遅延されるようになります.StrictData
下で宣言された
data T = Normal Int | Strict !Int | Lazy ~Int
というデータ型は,通常のHaskellの以下のデータ型と同等になります.
+data T = Normal !Int | Strict !Int | Lazy Int
Strict
拡張は,StrictData
拡張に加え,ほとんどのパターンを暗黙的にバンパターンにする拡張です.つまり,殆どの評価を正格にする拡張です.バンパターンに変わる箇所は,関数の引数,let
/where
句の束縛変数,case
式のパターンマッチなどです.これらのパターンには,最外の場所に!
が暗黙的に付与されます.例えば,Strict
拡張下で定義された
f :: Int -> (Int, Int) -> Int
+= let zy = z * y in case x - z of z' -> z' ^ z f x (z, y)
という関数は,BangPatterns
拡張下のHaskellの以下の関数と同等になります.
f :: Int -> (Int, Int) -> Int
+!x !(z, y) = let !zy = z * y in case x - z of !z' -> z' ^ z f
注意して欲しいのは,このバンパターンはseq
に置き換わるため,WHNFまでしか評価されないということです.つまり,!(z, y)
というパターンは単なる(z, y)
と完全に同じです.またトップレベルの束縛にバンパターンを付与することは許されておらず,遅延されるということにも注意が必要です.
この節では,以下の2つの拡張を紹介します.
+ViewPatterns
: ユーザーガイド - ViewPatterns拡張PatternSynonyms
: ユーザーガイド - PatternSynonyms拡張GHC拡張では,Haskell標準のパターンをさらに強力なものにする拡張があります.ViewPatterns
はビューパターンという新たなパターンを導入します.また,PatternSynonyms
はパターンの別名を付けることができるようにする拡張です.
Haskell標準にあるパターンガードは,非常に強力ですが,表現が非常に冗長になる場合があります.これを短縮して書けるように,ViewPatterns
拡張はビューパターンというものを導入します.ビューパターンは,->
の左側に式を,右側にパターンを書くことで,左の式に対象を適用して結果が右側のパターンにマッチした時,マッチするようなパターンです.例えば,
`mod` 2) -> 0) = Nothing
+ f ((= Just x f x
というように使用でき,f 0
はNothing
を,f 3
はJust 3
をそれぞれ返すようになります.この関数宣言は,以下のパターンガードを用いて書いた関数と一致します.
| 0 <- x `mod` 2 = Nothing
+ f x = Just x f x
ビューパターンはHaskellのcase
式の翻訳ルールに次の規則を加えることで実現されます.
case v of { (e -> p) -> e1; _ -> e2 }
+case (e v) of { p -> e1; _ -> e2 } ≡
PatternSynonyms
拡張は,非常に強力で大きな拡張です9.PatternSynonyms
拡張は名前の通り,パターンに別名を与えるパターンシノニム機能を提供します.パターンシノニムは通常の関数と同じように,次のように定義できます.
pattern Nil :: [a]
+pattern Nil = []
+
+pattern Cons :: a -> [a] -> [a]
+pattern Cons x xs = x : xs
+
+{-# COMPLETE Nil, Cons #-}
このように定義したパターンは,以下のように使用できます.
+len :: [a] -> Int
+Cons _ xs) = 1 + len xs
+ len (Nil = 0 len
パターンシノニムは非常に便利な機能ですが,一方で注意する事項も幾つかあります.
+まず,パターンシノニムの定義は関数定義と非常に似ていますが,パターンの別名であることに注意してください.パターンシノニムの定義において変数が出現する場合,関数の引数のように錯覚してしまいがちですが,この変数にはパターンにマッチした時そのマッチした部分が当てがわれます.つまり,右の式でマッチしたものが左の変数に束縛されるため,左の変数に束縛された後右の式を実行する関数と,流れが逆になるということです.このため,パターンシノニムの引数の変数は必ず右に出現する必要があります.また,パターンシノニムの右側には変数を含むパターンしかかけません.そのため,式を書きたい場合,ViewPatterns
拡張などを用いなければなりません.さらにパターンシノニムは,デフォルトではパターンの網羅性検査が非常に難しいため,網羅性検査を行わないようになっています.ただし,COMPLETE
プラグマを用いてパターンシノニムの網羅条件を与えることで,その範囲で網羅性検査を行うようになります.
パターンシノニムはパターンの種類に応じて3種類の書き方が存在します.上の単純なパターンシノニムは,双方向(bidirectional)パターンシノニムと呼ばれ,暗黙的にパターンの名前と等しい関数が作られます.この関数を用いることで,[0, 1, 2]
の代わりにCons 0 (Cons 1 (Cons 2 Nil))
といった式も書くことができるようになります.ただし,このような関数が単純には作れないパターンも存在します.例えば,(x, _)
というパターンに,First x
というパターンシノニムを与えたい場合,このFirst
に対する関数は_
の部分に入れるべき値が分からないため,作りようがありません.このような関数が単純に作れないパターンシノニムは単方向(unidirectional)パターンシノニムと呼ばれ,双方向パターンシノニムが=
を使って定義されるのに対し,次のように<-
を使って書きます.
pattern First :: Int -> (Int, Bool)
+pattern First x <- (x, _)
このパターンシノニムはFirst
という関数は作らず,単純にパターンの別名だけを提供します.ただし,First
関数の定義を次のように与えることが可能になっています.
pattern First :: Int -> (Int, Bool)
+pattern First x <- (x, _)
+where
+ First x | x < 0 = (x, False)
+ First x = (x, True)
また,パターンシノニムはパターンの評価順序にも注意する必要があります.例えば,次の例をみてください.
+data Pair a b = Pair a b
+
+type Pair3 a b c = Pair a (Pair b c)
+
+pattern Pair3 :: a -> b -> c -> Pair3 a b c
+pattern Pair3 x y z = Pair x (Pair y z)
+
+f :: Pair3 Bool Bool Bool -> Bool
+Pair3 True True True) = True
+ f (= False
+ f _
+f' :: Pair3 Bool Bool Bool -> Bool
+Pair True (Pair True True)) = True
+ f' (= False f' _
このf
とf'
は評価順が異なり,f (Pair False undefined)
が例外を投げるのに対し,f' (Pair False undefined)
はFalse
を返します.これは,パターンシノニムを使ったパターンマッチでは,自身のパターンを先に調べ,次に引数のパターンマッチを行うからです.つまり,f
は以下と同等になります.
f :: Pair3 Bool Bool Bool -> Bool
+Pair x (Pair y z)) | True <- x, True <- y, True <- z = True
+ f (= False f _
パターンシノニムは,モジュールエクスポートを書く際にも注意が必要で,module A (pattern Cons, pattern Nil) where ...
というように接頭にpattern
をつける必要があります.
この節では,以下の4つの拡張を紹介します.
+DuplicateRecordFields
: ユーザーガイド - DuplicateRecordFields拡張OverloadedLabels
: ユーザーガイド - OverloadedLabels拡張NamedFieldPuns
: ユーザーガイド - NamedFieldPuns拡張RecordWildCards
: ユーザーガイド - RecordWildCards拡張Haskellのレコード構文は,便利な反面幾つか機能が劣る場面もあります.このため,GHCでは,レコードをより扱いやすくするための拡張を幾つか提供しています.それが,DuplicateRecordFields
,OverloadedLabels
,NamedFieldPuns
,RecordWildCards
の4つの拡張です10.
Haskell標準では,同じモジュール内で同じフィールド名を持つ複数のレコード構文を使用したデータ型の定義を行うことができません.これはどのデータ型のフィールドかが曖昧であるようなプログラムを書けてしまうからですが,そういう状況に遭遇するとこの制約は非常に不便です.これを解決するのが,DuplicateRecordFields
拡張です.DuplicateRecordFields
拡張は,曖昧になるような式を書けなくする代わりに,同一モジュールの複数のデータ型が同じフィールド名を持つことを許容する拡張です.つまり,以下のようなことが可能になります.
data A = A { d :: Int }
+data B = B { d :: Bool }
ただし,この拡張下では,曖昧なフィールドを用いたレコードのアップデート構文やフィールドの選択関数の使用の際は型を明記する必要があったり,モジュールのエクスポートリストで選択関数をエクスポートすることが出来なくなったりします.
+OverloadedLabels
拡張は,#foo
というような#
から始まる新たな構文を導入します.#foo
はGHC.OverloadedLabels
モジュールのfromLabel
メソッドにおいてIsLabel "foo" a => a
というような型を持つ場合と同等になります.これを用いることで,同じフィールドを持つデータ型に対する選択関数を次のように書けます11.
{-# LANGUAGE OverloadedLabels #-} -- the main extension
+{-# LANGUAGE DataKinds #-} -- for Symbol kind
+{-# LANGUAGE KindSignatures #-} -- for HasField's `l` parameter
+{-# LANGUAGE MultiParamTypeClasses #-} -- for HasField and IsLabel classes
+{-# LANGUAGE FunctionalDependencies #-} -- for HasField class
+{-# LANGUAGE FlexibleInstances #-} -- for HasField instances
+{-# LANGUAGE ScopedTypeVariables #-} -- for the IsLabel instance
+{-# LANGUAGE DuplicateRecordFields #-} -- for A and B data types
+
+import GHC.OverloadedLabels (IsLabel(..))
+import GHC.TypeLits (Symbol)
+import Data.Proxy (Proxy(..))
+
+data A = A { d :: Int }
+data B = B { d :: Bool }
+
+class HasField a (l :: Symbol) b | a l -> b where
+ selectField :: Proxy l -> a -> b
+
+instance HasField A "d" Int where
+A x) = x
+ selectField _ (
+instance HasField B "d" Bool where
+B x) = x
+ selectField _ (
+instance HasField a l b => IsLabel l (a -> b) where
+= selectField (Proxy :: Proxy l) fromLabel
これを使うことで,#d A { d = 0 }
は0
を,#d B { d = True }
はTrue
を返してくるようになります.また,#d
には型を明記しなくても型推論が働くようになります.
さて他にレコードのパターンマッチやコンストラクトを非常に便利にしてくれる拡張として,NamedFieldPuns
拡張とRecordWildCards
拡張があります.レコードのパターンマッチは多くの場合冗長になりがちで,次のようなボイラープレートを書きがちです.
data A = A { x :: Int, y :: Bool }
+
+f :: A -> Int
+A{ x = x } = x + 1 f
NamedFieldPuns
拡張は,同等のことを次のように書けるようにする拡張です.
f :: A -> Int
+A{ x } = x + 1 f
また,このパターンは旧来の書き方と合わせて書くこともできます.
+g :: A -> Int
+A{ x, y = False } = - x
+ g A{ x } = x g
さらにこの拡張は,コンストラクトの際も役に立ちます.let x = 1 in A { x, y = True }
と書くとこの式は,A { x = 1, y = True }
と書くのと同等になります.
NamedFieldPuns
拡張ではフィールド名を明記する必要がありましたが,RecordWildCards
拡張はさらにフィールド名を明記する必要がなくなります.以下のように{..}
と書くことで,全てのフィールドを展開してくれるようになります.
f :: A -> Int
+A{..} = x + 1 f
また,部分的に明記することも可能で,その場合以下のように書きます.
+g :: A -> Int
+A{ y = False, ..} = -x
+ g A{..} = x g
コンストラクトの際も,この拡張は有効です.let x = 1 in A { y = True, ..}
と書いた場合,A { x = 1, y = True }
と書くのと同等になります.
この節では,以下の拡張を紹介します.
+TypeOperators
: ユーザーガイド - TypeOperators拡張Haskellではユーザー定義の関数やデータ型のコンストラクタにおいて,演算子表記のものも定義できるようになっています.例えば,以下のようにです.
+data Pair a b = a :*: b
+infixl 7 :*:
+
+(&) :: a -> (a -> b) -> b
+& f = f x
+ x infixl 1 &
しかしHaskell標準では,型を定義する場合そのようなことはできません.これを可能にするのが,TypeOperators
拡張です.この拡張の有効下では,
type a + b = Either a b
+infixr 5 +
ということが可能になります.ただし,このように定義した型演算子は,同じ名前の値としての演算子があった場合区別ができません.このため,モジュールのエクスポートリストを書く際,型演算子か値レベルの演算子かの区別が付かなくなった場合,値レベルの方が優先されます.この時,型演算子を明示したい場合,type
を付けます12.
{-# LANGUAGE TypeOperators #-}
+
+module A
+type (+)
+ ( where
+ )
+type a + b = Either a b
この節では,以下の4つの拡張を紹介します.
+MultiParamTypeClasses
: ユーザーガイド - MultiParamTypeClasses拡張FlexibleContexts
: ユーザーガイド - FlexibleContexts拡張FlexibleInstances
: ユーザーガイド - FlexibleInstances拡張InstanceSigs
: ユーザーガイド - InstanceSigs拡張Haskellの型クラスは非常に強力な機構です.しかしながら,Haskell標準の型クラスの構文は非常に制約がきつく,これらを緩和したいと思うことがよくあります.このため,GHCでは制約を緩和する拡張をいくつか提供しています.それが,MultiParamTypeClasses
,FlexibleContexts
,FlexibleInstances
,InstanceSigs
の4つの拡張です.
Haskell標準では,クラスは1つの変数しか持てません.なので,次のような型クラスは作れません.
+class C a b
これは非常に不便な制約なため,複数のパラメータを使うような型クラスを許容する拡張がMultiParamTypeClasses
拡張です.この拡張により,上のコードが許容されるようになる他,以下のように変数が全くない型クラスも宣言することができるようになります.
class Nullary
また,Haskell標準では,メソッドにおいてクラスの型変数に型制約をかけるということも許容されていませんが,MultiParamTypeClasses
拡張ではこれも可能にします13.これによって以下のようなクラス定義も書けるようになります.
class Setable s a where
+ elem :: Eq a => a -> s a -> Bool
Haskell標準では,型制約の解決を安全に,しかも単純にするために,型注釈における制約の書き方やクラス定義,インスタンス定義の際の制約の書き方を大きく制限しています.しかし,より複雑な型制約を書きたい時が往々にしてあります.そこで,この制限を緩め,クラス階層が非循環である場合には許容するようにする拡張が,FlexibleContexts
拡張です.この拡張下では,
-- valid
+class (Monad m, Monad (t m)) => Transform t m where
+ lift :: m a -> (t m) a
+
+-- valid
+f :: Functor Maybe => ()
+= ()
+ f
+-- invalid
+class A a => B a
+class B a => A a
となります.
+FlexibleInstances
拡張もFlexibleContexts
拡張と同じく,Haskell標準での型クラスインスタンスの書き方の制限を,停止制限を守る場合に許容するというように緩和する拡張です.停止制限は簡単に言ってしまえば,インスタンス宣言において,型制約がインスタンスより小さく14,型関数を使っていないというものです15.この拡張下では,
-- valid
+instance C1 (Maybe [a])
+
+-- valid
+instance C2 a a => C2 [a] [a]
+
+-- valid
+instance (Eq a, Show b) => C3 a b
+
+-- valid
+instance (Show a, Show (s a)) => Show (S s a)
+
+-- invalid
+instance C4 a => C4 a
+
+-- invalid
+instance C2 a a => C1 [a]
+
+-- invalid
+instance Functor [] => C1 a
となります.また,この拡張下では,型シノニムをインスタンスにすることもできます16.
+type List a = [a]
+
+-- Instead of `instance C [a]`
+instance C (List a)
ただし,型シノニムを使う場合そのシノニムの引数は全て適用しなければならないことに注意が必要です.
+Haskell標準では,型クラスインスタンスの定義時,そのメソッドの型注釈は書けないようになっています.しかし,複雑な型クラスインスタンスを書く際,メソッドの型注釈を書きたい場合があります17.これを可能にするのがInstanceSigs
拡張です.InstanceSigs
拡張の元では,以下のようなインスタンス宣言が書けます.
data A = A
+
+instance Eq A where
+ (==) :: A -> A -> Bool
+A == A = True
この節では,以下の拡張を紹介します.
+NamedWildCards
: ユーザーガイド - NamedWildCards拡張GHCには型ワイルドカードという機能があります.この機能は,_
と型シグネチャ上で書いておくと,そこの部分の型を推論してエラーメッセージとして表示してくれる機能です.この機能は,以下のように部分的に記述したり複数指定したりすることも可能です.
-- Inferred type: (a, b) -> (a, Maybe a1)
+ignoreSecond :: _ -> _
+= (x, Nothing) ignoreSecond (x, _)
これを活用すれば,複雑な型をある程度ヒントを与えた状態で推論してもらい,型を追記するプログラミングスタイルや,GHCが実際に型をどう推論するかを見るための補助に応用できます.しかし,例えばignoreSecond
が引数と返り値で型が同じであるという情報が分かっていた場合に,これをヒントとして伝えたい場合がありますが,型ワイルドカードでそれを伝える方法はありません.これを解決するのがNamedWildCards
拡張です.この拡張を使うと,以下のようなプログラムに対しても,接頭に_
が付いている型をワイルドカードとみなして,エラーメッセージで型の推論結果を表示してくれるようになります.
-- Inferred type: (a, Maybe a1) -> (a, Maybe a1)
+ignoreSecond :: _a -> _a
+= (x, Nothing) ignoreSecond (x, _)
この節では,以下の2つの拡張を紹介します.
+Arrows
: ユーザーガイド - Arrows拡張RecursiveDo
: ユーザーガイド - RecursiveDo拡張Haskellでは,モナドを扱いやすくするための,do構文という専用の構文が用意されています.この構文はHaskellプログラミングにおいて広く利用されています.GHCでは,これに加えArrow
とMonadFix
というクラスに対しての専用の構文も提供しています.これはGHC拡張で実装されており,それぞれArrows
拡張,RecursiveDo
拡張を有効にすることで使用可能です.
Arrow
クラスは,モナドの一般化として導入されました18.このクラスには,モナドのdo
構文と同様に,クラスメソッドだけの式に脱糖できる構文が考案され,GHC拡張として実装されています.それがArrows
拡張で利用できるproc
構文です.
例えば,Arrow
クラスのメソッドを使った次のような関数は,
doSomething :: Arrow a => a Int Int -> a Int Int -> a Int Int -> a Int Int
+
+ doSomething f g h= arr (\x -> (x + 1, x))
+ >>> first (f >>> (arr (\y -> 2 * y) >>> g) &&& returnA >>> arr snd)
+ >>> arr (\(y, x) -> (x, x + y))
+ >>> arr (\(x, z) -> (z, x * z))
+ >>> second h
+ >>> arr (\(z, t) -> t + z)
proc
構文を使うと,
doSomething :: Arrow a => a Int Int -> a Int Int -> a Int Int -> a Int Int
+= proc x -> do
+ doSomething f g h <- f -< x + 1
+ y -< 2 * y
+ g let z = x + y
+ <- h -< x * z
+ t -< t + z returnA
というように書けます19.また,ArrowLoop
クラスのloop
メソッドに変換される,rec
構文も搭載されており次のようなフィードバック制御を相互再帰で行うプログラムを書くことができます.
counter :: ArrowLoop a => (Int -> a Int Int) -> a Bool Int
+= proc reset -> do
+ counter delay <- returnA -< if reset then 0 else next
+ rec output <- delay 0 -< output + 1
+ next -< output returnA
proc
構文についてはArrow syntaxのページにまとめられている他,提案論文にて変換規則を確認することが可能です.
さて,もう1つのMonadFix
クラスは,モナドを拡張し,再帰的なバインディングを許すようなものです.このクラスを元に,RecursiveDo
拡張はdo構文をさらに拡張します.具体的には,次のように使用できるrec
という構文を新たに導入します.
doSomething :: [Int]
+= do
+ doSomething <- [y, y * 10]
+ rec x <- [1, 2]
+ y pure $ x + y
この関数は,次のようにMonadFix
クラスのメソッドmfix
を使った関数と同等です.
doSomething :: [Int]
+= do
+ doSomething <- mfix $ \~(x, y) -> do
+ (x, y) <- [y, y * 10]
+ x <- [1, 2]
+ y pure (x, y)
+ pure $ x + y
また,rec
を省略して書けるmdo
という構文も提供されます.
doSomething :: [Int]
+= mdo
+ doSomething <- [y, y * 10]
+ x <- [1, 2]
+ y pure $ x + y
mdo
構文は,それぞれの文と変数の依存関係を解析し,自動的にrec
ブロックに分けてくれます.後は,その分けられたrec
文をmfix
に翻訳することで,通常のdo
構文に翻訳することができます.例えば,
+ mdo<- m
+ a <- f a c
+ b <- f b a
+ c <- h a b
+ z <- g d e
+ d <- g a z
+ e pure c
という式は,
+do
+<- m
+ a <- mfix $ \~(b, c) -> do
+ (b, c) <- f a c
+ b <- f b a
+ c pure (b, c)
+ <- h a b
+ z <- mfix $ \~(d, e) -> do
+ (d, e) <- g d e
+ d <- g a z
+ e pure (d, e)
+ pure c
という式に翻訳されます.mdo
とrec
の変換規則は,提案論文にて確認が可能です.
今回は,GHC拡張の簡単な紹介と使い方について,それから個人的にデフォルトで有効化している,Preludeの暗黙的なインポートを抑制する拡張,新たな構文を導入する拡張を紹介しました.
+次回は,他のデフォルトで有効化している拡張について紹介したいと思います.
+この記事では特に断らない限り,Haskell2010を「Haskell標準」または「Haskell」と呼称します.↩︎
このオプションは,拡張を無効にするGHC拡張(例えば,NoImplicitPrelude
拡張など)も含めて表示します.実際にはNo
が付いている拡張を抜くと,提供されている数は120個になります.↩︎
Haskell標準では,ある拡張を無効にするといった機能は提供されていません.このため,GHCでは無効にする機能を1つの拡張として,Haskell標準に則った形で提供しています.↩︎
有効にする拡張と無効にする拡張を両方指定した場合,GHCは指定された順番に沿って最後に指定された方を拡張として採用します.↩︎
Haskell2010標準では,Haskell2010
というプラグマをサポートすること,またHaskell98から新たにHaskell2010までに取り込まれた機能を切り離したPatternGuards
/NoNPlusKPatterns
/RelaxedPolyRec
/EmptyDataDecls
/EmptyDataDecls
という拡張をそれぞれサポートすることが望ましいと規定されています.GHCもHaskell2010
という拡張を指定できるようになっており,ここにあるほとんどはこの拡張を有効にした場合にも有効になります.↩︎
デフォルトで有効になる拡張のほとんどは,Haskell 2010を元にしたものです.ただし全てがそうというわけではありません.NondecreasingIndentation
はHaskell標準にはない機能です.またGHCはHaskell 2010で規定されている仕様を全てデフォルトで取り込んでいる訳でもありません.特にHaskell標準ではデータ型の宣言に型制約を書くことができますが,GHCではデフォルトではできません.これを有効にする場合,DatatypeContexts
拡張を有効にする必要があります.↩︎
GHCの内部ではRelaxedPolyRec
という拡張も一緒に有効になります.しかし,現在この拡張は実装上の問題でGHC上で無効にすることができないため,ドキュメント上からも削除されています.この記事でもGHCの方針に従って,この拡張は特に扱いませんのでご留意ください.↩︎
現在,Preludeの代替を目指す,rioというパッケージが作成されています.このパッケージは現在まだprereleaseの段階で,stackにおいて実験的に使用されています.様々な最新のHaskellプログラミングの知見を取り入れており,標準のPreludeに大きく拡張を施しているため,Haskellで大規模な開発を行う場合注目する価値があるかもしれません.↩︎
GHC 8.2.2の段階では,パターンシノニムはコンパイラがクラッシュするなどの非常に多くのバグを抱えていました.私は8.4.2をまだあまり試していませんが,パターンシノニムの仕様が非常に複雑なため,8.4.2でもまだバグを多く抱えている可能性があります.パターンシノニムをプロダクトで多用する場合,その点に注意した方が良いでしょう.↩︎
GHCのレコードシステムの拡張は非常に強力ですが,その反面システムが非常に複雑になっています.このため,8.2.2の段階でコンパイラがクラッシュするなど非常に多くのバグを抱えていました.レコードシステムの仕様の改良は現在も進んでいますが,8.4.2でもまだバグを多く抱えている可能性があります.これらの拡張をプロダクトで多用する場合,その点に注意した方が良いでしょう.特に,GHC 8.0以降に導入された拡張には注意が必要です.↩︎
OverloadedLabels
拡張はかなり最近入った拡張で,多数のGHC拡張,特に強力な型システムを前提にして書かれています.このため,選択関数の実装にもかなり多くのGHC拡張を使用しています.ここでは,特に解説しないのでそういうものだと思っておいてください.なお,このプログラムはプロダクションで使うことを前提にしていませんので,そこはご注意ください.↩︎
この機能は型演算子を定義しないで再エクスポートなどをする場合にも使用されるため,ExplicitNamespaces
拡張として切り離されています.↩︎
この機能はConstrainedClassMethods
拡張として切り離されており,MultiParamTypeClasses
拡張を有効にすると一緒に有効になります.↩︎
型制約が小さいとは,型変数とコンストラクタと変数の組の出現が少ないということです.↩︎
より正確には,FunctionalDependencies
に対する制限もありますが,ここでは割愛します.↩︎
この拡張は,TypeSynonymInstances
拡張として切り離されており,FlexibleInstances
拡張を有効にすると一緒に有効になります.↩︎
特にScopedTypeVariables
拡張を指定する場合,型注釈は必要です.↩︎
“Generalising Monads to Arrows”, John Hughes, in Science of Computer Programming 37, pp. 67–111, May 2000↩︎
一見,この構文は単純な脱糖を行うと脱糖後のプログラムが非常に冗長になるように思えます.しかし,Arrow
クラスのメソッドに設けられている書き換え規則によって,最終的に妥当な大きさまで脱糖後のプログラムが小さくなってくれます.↩︎
前回の更新からちょっと時間が空いてしまいました 💦
+小ネタです。掲題の通りderiveJsonNoPrefixというパッケージをリリースしました。
+地味に有用だと思うので、READMEをやや意訳気味に翻訳して記事にします。
+十分に単純なので、仕様が変わることもまさかないでしょうし。
以下、こちらのコミットの時点のREADMEの翻訳です。
+プレフィックスに優しいToJSON
とFromJSON
のインスタンスを定義するTemplate Haskellのマクロを提供します。
こんな感じのJSONを作りたいとしましょう:
+{
+"id": "ID STRING",
+ "max": 0.789,
+ "min": 0.123
+ }
きっとToJSON(おそらくそれに加えてFromJSONも)のインスタンスを自動的に定義するための、次のようなレコード型を定義したくなるでしょう。
+{-# LANGUAGE TemplateHaskell #-}
+
+import Data.Aeson.TH
+
+data SomeRecord = SomeRecord
+ id :: String
+ { max :: Double
+ , min :: Double
+ ,deriving (Eq, Show)
+ }
+$(deriveToJSON defaultOptions ''SomeRecord)
しかし、こんなレコード型は定義すべきではありません。
+id
もmax
もmin
も、Prelude
に定義済みなのですから!
この問題を回避するために、レコードラベルに型の名前をプレフィックスとして加える、ということをわれわれはよくやります。
+data SomeRecord = SomeRecord
+ someRecordId :: String
+ { someRecordMax :: Double
+ , someRecordMin :: Double
+ ,deriving (Eq, Show) }
そして、deriveToJSON
にデフォルトと異なるオプションを渡して実行します。
= firstLower . drop (length "SomeRecord") } ''SomeRecord
+ deriveToJSON Json.defaultOptions { fieldLabelModifier
+firstLower :: String -> String
+:xs) = toLower x : xs
+ firstLower (x= error "firstLower: Assertion failed: empty string" firstLower _
fieldLabelModifier
オプションは文字通り、対象のレコードをJSONに変換するとき、あるいはJSONから対象のレコードの値に変換する時、レコードのラベルを変換する関数を設定するために使います。
+👆の場合、プレフィックスであるSomeRecord
の文字数分レコードラベルからdrop
して、先頭の文字(someRecordId
で言えばId
のI
に相当します)を小文字に変換しているのがわかるでしょうか?
そう、これがderiveToJsonNoTypeNamePrefix
がやっていることとほぼ同等のことです。
+deriveToJsonNoTypeNamePrefix
は、実質次のように定義されています。
deriveToJsonNoTypeNamePrefix :: Name -> Q [Dec]
+=
+ deriveToJsonNoTypeNamePrefix deriver name = dropPrefix name } name
+ deriveToJSON Json.defaultOptions { fieldLabelModifier
+dropPrefix :: Name -> String -> String
+= firstLower . drop (length $ nameBase name)
+ dropPrefix name
+firstLower :: String -> String
+:xs) = toLower x : xs
+ firstLower (x= error "firstLower: Assertion failed: empty string" firstLower _
結果、これからはfieldLabelModifier
をもう自分で定義する必要がありません!🙌
import Data.Aeson.DeriveNoPrefix
+
+$(deriveJsonNoTypeNamePrefix ''SomeRecord)
👆 のderiveJsonNoTypeNamePrefix
は deriveJSONと同様に、ToJSON
とFromJSON
のインスタンス、両方を生成します。
+もちろん、FromJSON
のインスタンスを生成するときのオプションとしても、プレフィックスを削除するためのfieldLabelModifier
を渡してくれます!
ToJSON
・FromJSON
のインスタンスが定義されたextensible recordを提供するライブラリーなので、そうしたextensible recordを提供するライブラリーが学習コストや依存関係などの事情で「重たい」と感じたときにこのパッケージを使ってください。
+Haskellのコンパイラの1つであるGHCは、オープンソースソフトウェア(OSS)のプロジェクトとして今も活発に開発が進められています。 +個人の経験や経歴や肩書きや権限などに関わらず、誰でもGHCの開発にすぐに参加することができます。
+ここでは、GHCに新しい変更を提案し実装するための、以下の手順例を紹介します。
+ +GHCに改善したい点があれば、誰でも変更提案が可能です。
+提案のハードルは案外高いものではありません。GHC開発では、新たなcontributionが歓迎されています。
+仮に提案やパッチがreject判断されるとしても、GHCの開発者と直接やり取りする良い機会が得られます。
以下では、数値リテラルの構文を変更する単純な例をもとに、変更提案やパッチ送付の手順例を紹介します。(文章だらけになってしまいましたがご容赦を 😊 )
+GHCは、コンパイラ本体やライブラリやツールチェーンなど多くの要素で構成されていますが、ここではコンパイラ本体への変更提案の手順について紹介します。
+GHCのコンパイラ本体の開発では、ユーザーに見える(user-visible)振る舞い等を変更(追加・修正・削除など)するための提案(proposal)手順が定められています。 +事前の調整や権限などを必要とせず、GitHubへのpull requestを通じて誰もが提案できます。
+なお、変更提案(仕様)のプロセスと、修正パッチ送付(実装)のプロセスは、分離されています。必ずしも、変更提案者が実装まで行う必要はありません。
+提案の具体的な手続きについては、以下に記載されています。よく読んでおきましょう。
+ +変更提案は、提案書を書いて以下の場所(リポジトリ)に、pull requestを送ることで行えます。
+ +提案の流れは、ざくっと以下の通りです。
+数値リテラルの構文を変更する場合の、具体的な変更提案の例を紹介します。
+ +その他の提案の例は以下にたくさんあります。
+提案プロセスはGitHub上で行うものです。操作ミスがあったところでやり直しは何度でも行えます。失敗やミスを不必要に怖れる必要はありません。
+また、多くの提案はAcceptedに至らないこともあるので、結果を恥ずかしがる必要もありません。提案の結果に関わらず、提案とその議論自体が、他の開発者に新たな観点や気づき・刺激を提供できます。
それでは、提案プロセスをお楽しみ!
+GHCへの変更提案に対するコード修正は、パッチを作成して送付することにより行われます。 +ここでは、コード開発ツールであるPhabricatorのdifferential機能を用いる、標準的なパッチ送付の手順について紹介します。
+なお、修正パッチはGitHubのpull requestを通じても送付できますが、後のコードレビューのフェーズを考慮すると、Phabricatorを用いるこの手順が効率的です。
+パッチ作成から送付についての具体的な手続きについては以下に記載されています。
+ +また、Phabricatorの詳細な操作手順については、以下に解説記事があります。
+ +パッチ送付の流れは、ざくっと以下の通りです。
+数値リテラルの構文を変更する場合の、具体的なパッチ送付の例を紹介します。
+ +その他のレビュー中パッチの例は以下にたくさんあります。
+ +パッチ送付は、Phabricatorやgitの機能を用いて行うものです。操作ミスがあったところで、GHCのリポジトリ本体に直ちに反映されるわけではありません。やり直しは何度でも行えます。失敗やミスを不必要に怖れる必要はありません。communityのためになるcontributionは常に歓迎されています。
+それでは、パッチ送付プロセスをお楽しみ!
+わからないことがあれば、ghc-devsのMLに問い合わせると親切に教えてもらえます。 +もちろん、Haskell-jpのslackの#questionsチャネルなどで尋ねるのも良いでしょう。
+なお、GHCでの開発作業については、Working on GHCも参考にどうぞ。
+また、GHCの開発フロー全体については、こちらも参考にどうぞ。GHC関連のサイトの情報を力づくで検索するには、こちらもどうぞ。
Happy Hacking!
+以上です。
+こんにちはkakkun61こと岡本和樹です。
+去る11月10日にHaskell Day 2018が開催されましたので、そのイベントレポートをお送りします。
+https://haskell-jp.connpass.com/event/92617/
+Haskell Dayは2012年2016年と不定期に開催しており今回で3度目となります。
+2018年の今回は「Haskellちょっと興味あるからちょっとできるまで」というテーマで開催されました。
+開発中の対話的チュートリアル「作りながら学ぶHaskell入門」を使って、参加者の皆さんにもくもくと入門していただきました。「作りながら学ぶHaskell入門」では、簡単な課題を解くことで、Haskellの初歩的な使い方を学びつつ、学んだ知識をその場でテストできます。
+https://github.com/haskell-jp/makeMistakesToLearnHaskell
+スポンサーとしてIIJに飲食物の提供をしていただきました。
+約110名の方が出席し約40名の方が回答してくださいました。
+参加できなくなった約40名の方は事前のキャンセルをよろしくお願いします。
+ここ1年以内に始めた方が全体の21.4%、1年以上5年未満前に始めた方がちょうど半分程度となりました。1年以上5年未満前に始めた方の57%の方は今でもHaskellを使い続けているようです。
+5年以上前に始めた方は全体の1/4となりました。
+項目は下記となります。
+この項目にはアンケートの途中で追加したものもあるので、結果があまり厳密でないことに注意してください。
+その他の選択肢で自由記述では次の票がありました。
+参加者の発表者、スタッフのみなさんのおかげで無事開催することができました。ありがとうございました。
+今後ともHaskell-jpをよろしくお願いします。
+こんにちは。みなさん、テストは書いてますか?
+「Haskellライブラリ所感2016」という記事でも紹介されているとおり、Haskellにも様々なテスト用ライブラリーがあります。
+今回は、「Haskellライブラリ所感2016」でも紹介されているsilentlyというパッケージにインスパイアされた、新しいテスト用ライブラリーを作りました。
+タイトルにも書きましたがmain-testerといいます。
main-testerは名前の通り、main
関数のテストをサポートするライブラリーです。
+Haskell製のプログラムを起動すると最初に実行される、あのmain
関数です。
main
関数はIO ()
という型であるとおり、原則として必ず入出力を伴うので、自動テストがしにくい関数です。
+一般的なベストプラクティスとしては、できるだけIO
でない、純粋な関数を中心にテストを書いていくのが普通でしょう。
+それでも敢えてmain
関数の自動テストを書くのには、以下のメリットがあります。
main
関数をテストすると言うことは、作っているコマンドの、ユーザーの要求に最も近いレベルのテスト、E2Eテスト(end-to-end テスト)をすることができる。main
関数(や、その他のIO
を伴う関数)に対するテストは、データベースやファイルシステムなど、外部のソフトウェアとの「組み合わせ」で起こるバグを検出できる。
+このように、main
関数をはじめとする、IO
な関数に対して敢えて自動テストを書くことには、様々なメリットがあります。
+main-tester
はそうしたIO
な関数をテストする際に伴う、2つの問題を解決しました。
captureProcessResult
という関数で、標準出力・標準エラー出力に出力した文字列をそれぞれByteString
として取得することができます。withStdin
という関数で、標準入力に与えたい文字列をByteString
として与えることができます。ここに書いたことは、ビルドした実行ファイルを子プロセスとして呼び出すことによってもできます。
+入出力の順番など、標準出力や標準エラー出力のより細かい挙動をテストするにはその方がいいでしょう1。
+しかし、テストのためにPATH
を分離させる必要があったり、そのためにstack exec
を使ったらめっちゃ遅いという問題があったり、そもそも子プロセス呼び出しはそれだけでオーバーヘッドがあったりと、様々な問題があります。
+物事をよりシンプルにするには、main
関数を直接呼び出した方がよいでしょう。
+main-testerは、CLIアプリケーションのE2Eテストにおける、そうした子プロセスの呼び出しの問題と、より大きな関数をテストしたいというニーズに応えるためのライブラリーなのです。
「silentlyというパッケージにインスパイアされた」と冒頭で申しましたとおり、前節で紹介した機能は、実はすでにほかのライブラリーに似たものがあります。
+silentlyに加え、imperative-edslというパッケージに含まれる、System.IO.Fake
というモジュールです(ほかにもあったらすみません!🙇🙇🙇)。
+これらとmain-testerとの違いは何でしょう?
第一に、先ほども触れましたが、main-testerのcaptureProcessResult
関数やwithStdin
関数は、標準出力・標準エラー出力・標準入力でやりとりする文字列をstrictなByteString
でやりとりします。
+silentlyやSystem.IO.Fake
は、String
なのです。
+ByteString
は文字通り任意のバイト列を扱うことができるので、「Unicodeの文字のリスト」であるString
よりも、多様なデータを扱うことができます。
これは、特に複数の種類の文字コードを扱うとき、非常に重要な機能となります。
+以前の記事で取り上げた、Invalid character
というエラーを再現させる場合も、ないと大変やりづらいでしょう。
第二に、main-testerのcaptureProcessResult
関数は、main
関数の終了コードもExitCode
型の値として取得できます。
+main
関数の中でexitFailure
等の関数を呼び出すと、ExitCode
が例外として投げられます。
+既存のライブラリーでこれを行うと、ExitCode
が例外として処理されるため、テストしたいmain
関数の実行が終了してしまいます。
+結果、main
関数が標準出力・標準エラー出力に書き込んだ文字列を取得することができないのです。
+「○○というエラーメッセージを出力して異常終了する」といったことをテストしたい場合、これでは使いづらいでしょう。
+「main
関数のE2Eテストを行うためのライブラリーである」という観点から、必須の機能であると判断し、実装しました。
+ちなみに、ExitCode
以外の例外についてはそのまま投げられます。仕様を単純にするために、これはユーザーのテストコードの中で処理することとしています。
機能は非常にシンプルなので、使い方についてはドキュメントのサンプルコードを読めば大体わかるかなぁと思いますが、簡単にサンプルを載せておきましょう。
+例えばこんなソース👇のプログラムがあった場合、
+ExampleMain.hs:
+module ExampleMain where
+
+import Data.List
+import System.Exit
+
+main :: IO ()
+= do
+ main putStr "What's your name?: "
+ <- getLine
+ name if "Yuji" `isInfixOf` name
+ then putStrLn "Nice name!"
+ else die $ name ++ "? Sorry I don't know such a guy!"
main-testerを使えば、次のようにHspecでテストできます。
+ExampleSpec.hs:
+{-# LANGUAGE OverloadedStrings #-}
+
+import System.Exit
+import Test.Main
+import Test.Hspec
+import qualified ExampleMain
+import qualified Data.ByteString as B
+
+= hspec $
+ main "your-cool-command" $ do
+ describe "Given 'Yuji' to stdin" $
+ context "prints a string including 'Nice name' without an error" $ do
+ it <- withStdin "Yuji"$ captureProcessResult ExampleMain.main
+ result `shouldBe` ExitSuccess
+ prExitCode result `shouldSatisfy` B.null
+ prStderr result `shouldSatisfy` ("Nice name" `B.isInfixOf`)
+ prStdout result
+"Given other name to stdin" $
+ context "prints an error message" $ do
+ it <- withStdin "other name" $ captureProcessResult ExampleMain.main
+ result `shouldBe` ExitFailure 1
+ prExitCode result `shouldSatisfy` (not . B.null) prStderr result
それぞれのファイルを同じディレクトリーに置いた上で、次のように実行すれば試せるはずです (cabalユーザーの皆さんは適当に読み替えてください…)。
+> stack build hspec main-tester
+> stack exec runghc -- --ghc-arg=-i. ExampleSpec.hs
+
+your-cool-command
+Given 'Yuji' to stdin
+ prints a string including 'Nice name' without an error
+ Given other name to stdin
+ prints an error message
+
+Finished in 0.0130 seconds
+2 examples, 0 failures
バグを見つけたらこちらのGitLabのIssueに報告してください(最近の個人的な判官贔屓により、敢えてGitLabにしております 😏)。
+それではこの春はmain-testerでHappy Haskell Testing!! 💚💚💚
main
関数を子スレッドとしてforkIO
することで同じことが恐らくできますが、テスト結果の報告に使うべき、標準出力・標準エラー出力を食い合うことになってしまうので、非常にやりづらいと思います。↩︎
Haskell Antennaはlotz84氏が作ったHaskellの日本語情報を収集するウェブサイトです。 +下記の記事を読むと、動機付けなどが分かると思います。
+ +残念なことにHaskell Antennaは動作が重く、なかなか満足に閲覧することが出来ませんでした。 +そこで、Haskell Antennaをリニューアルしました!
+正確には、Planet Haskellの日本語版として作成したものを、新しいHaskell Antennaとして置き換えました。 +新Antennaは旧Antennaと比べると見た目も機能も更新頻度も残念なことになってしまいましたが、各サイトのフィードから記事の一覧を取得し静的サイトとして生成しているだけなので動作は軽快です。
+旧Antenna同様に新Antennaでも配信する情報源(今のところAtomかRSS2.0形式のフィード)をいつでも募集しています。 +もし追加すべき情報源にアイデアがあればGitHubレポジトリのREADMEにかかれている方法を参考にPull Requestを送っていただくことが可能です。 +また、PRを送るのは面倒だという方はHaskell-jpのSlackの#antennaチャンネルを通じて提案を行ってもらうことも大歓迎です。 +(Planet Haskellがそうであるように)Haskell中心でなくても良いので、Haskellの情報を発信しているブログを持っている方は是非、追加提案をしていただけると助かります。
+先日私はプリキュアハッカソン NewStageというちょっと変わったイベントで、「タイプセーフプリキュア!」の最近の更新について発表いたしました。
+今回はその際使用したスライドを、ブログ記事として拡大して共有させていただきたいと思います!
rubicureやACME::PrettyCureのような「プリキュア実装」の1つです。
+詳しくはこれから挙げる過去の記事をご覧ください、と言いたいところですが、よくよく見たら「プリキュア実装」が何かを明記してる記事ではないようなので😅、ここで軽く説明しましょう。
+「プリキュア実装」とは一言で言うと「プリキュアやプリキュアに変身する女の子たち、変身時の台詞など諸々のプリキュアの設定をソースコードに収録したライブラリー」です。
例えば、今回取り上げます私の「タイプセーフプリキュア!」は(もちろん)Haskellで書かれたプリキュア実装で、次のように書くことで、キュアアンジュが変身する際の台詞を取得することができます。
+(出力されるリストは、手で整形しています)
> import ACME.PreCure
+-- キュアアンジュには、薬師寺さあやが「ミライクリスタル・ブルー」を
+-- セットした「プリハート」を使うことで変身します。
+> transformationSpeech Saaya (PreHeart MiraiCrystalBlue)
+"ミライクリスタル!"
+ [ "ハートキラっと!"
+ , "は~ぎゅ~~!"
+ , "ぎゅ~!"
+ , "ぎゅ~~!"
+ , "輝く未来をー、抱きしめて!"
+ , "みんなを癒す!知恵のプリキュア!キュアアンジュ!"
+ , ]
GHCiで上記のコードを試す場合は、下記のコードでtypesafe-precureとunicode-showをインストールした上で起動するとよいでしょう。
+$ stack build typesafe-precure unicode-show
+$ stack exec ghci -- -interactive-print="Text.Show.Unicode.uprint"
その他の機能や、使っているGHCの拡張などについては下記の記事をご覧ください。
+ +そんな「タイプセーフプリキュア!」ですが、前述のQiitaの記事の最後で「typesafe-precureは現状非常に冗長で、非実用的な実装になってしまっています」と述べているとおり、ほかのプリキュア実装と異なり、実用性を度外視して「設定の正しさ」を最優先事項とした結果、変身時の台詞や浄化技(「必殺技」ともしばしば呼ばれます)の台詞を取得するのに、非常に冗長なコードが必要になってしまいました。
+それではせっかくYouTubeやらWikipediaやらBlu-rayやらを見直してせっせと集めた情報が勿体ないので、集めた情報を、コンパイル時にJSONとして出力することにしました。
+そうして生まれたのがcure-index.jsonとそれをプリティープリントしたpretty-cure-index.jsonです。
+将来的には、かつてrubicureで作ったユナイトプリキュアを書き直すのに使用しようかと考えています。
作るに当たって新たに「タイプセーフプリキュア!」のソースコードに仕込んだ仕組みについては、去年のHaskell Advent Calendarの記事をご覧ください。
+Template HaskellやGHCのANN
という機能を濫用することで達成しました。😎
従来のcure-index.jsonには、最新作である「HUGっと!プリキュア」と、その一つ前の作品である「キラキラ☆プリキュアアラモード」の情報しか収録されていませんでした。
+前述の去年のHaskell Advent Calendarの記事でも触れましたが、収録のためにはプリキュアの設定の書式を大幅に変更しなければならず、面倒なのでひとまず後回しにしていたのです。
そこで今年のプリキュアハッカソンにて発表するのによいネタだろうと思い、あの手この手を使って、全シリーズをcure-index.jsonに含める対応を行いました1🎉。
+それでは、具体的にどんな修正を行ったのか紹介しましょう。
+修正前は、プリキュアの設定を収録した各モジュール(ACME.PreCure.Textbook
以下にあるので、今後は「各Textbook
モジュール」と呼びます)には👇こんな感じのTypes.hsがたくさんありました。
data CureMiracle = CureMiracle deriving (Eq, Show)
+
+
+ transformedInstance
+ [t| CureMiracle |]
+ cureName_Miracle
+ introducesHerselfAs_Miracle variation_Dia
上記はキュアミラクルを表す型の定義と、その日本語での名前、変身時の名乗りといったプロフィールを設定しているコードです。
+このほかにも、プリキュアに変身する女の子の設定や、変身の際に必要な変身アイテムなどの型定義がたくさんあります。
+transformedInstance
で始まる行は、Template Haskellを使った、型クラスのインスタンス宣言です。
+transformedInstance
というマクロが、Transformed
という型クラスのインスタンスを生成することで、プリキュアを表す型と、日本語での名前、変身時の名乗りを実際に紐付けているのです。
+(実際の日本語での名前はご覧のとおりcureName_Miracle
といった変数に束縛されております。Words.hsというファイルから参照しています)
修正前はこのように、あくまでもHaskellのソースコードとして、プリキュアの設定を書いていたため、このままではcure-index.jsonのデータとして扱うのが難しい状態でした。
+そのため、今回修正した後の各Textbook
モジュールでは、👇こんな感じのProfiles.hsで、各種の設定を宣言することにしました。
transformees :: [Transformee]
+=
+ transformees
+ [ mkTransformee"Cure Miracle"
+ ""
+
+ cureName_Miracle
+ variation_Dia
+ introducesHerselfAs_Miracle...
+ , ]
mkTransformee
関数で作っているTransformee
型の値は、cure-index.jsonの一部として、JSONに変換する中間データです。もちろんToJSON
のインスタンスになっております。
+このように新しい各Textbook
モジュールでは、直接Haskellのソースコードとしてプリキュアの設定を書く代わりに、一旦JSONに変換する用の中間データを設けることで、cure-index.jsonに収録しやすい状態にしています。
こうして作られたTransformee
などの中間データ用の値は、各Textbook
モジュールのルートに当たるモジュールで、型クラスのインスタンス宣言を行ったり、ANN
という機能でモジュールに紐付けられます。
+以下は「魔法つかいプリキュア!」のルートに当たるモジュールMahoGirls.hs
からの抜粋です。
module ACME.PreCure.Textbook.MahoGirls where
+
+import ACME.PreCure.Textbook.MahoGirls.Profiles
+
+...
+
+{-# ANN module transformees #-}
+$(declareTransformees transformees)
+
+...
Profiles.hs
で定義したtransformees
というリストを、ANN
でMahoGirls
モジュールに紐付け、declareTransformees
というTemplate Haskellのマクロで型宣言やインスタンス宣言を生成するのに使っています。
+ANN
については前回の「タイプセーフプリキュア!を支える技術」をご覧ください2。
修正前との違いにおける要点を繰り返しましょう。修正後の各Textbook
モジュールでは、
するようにしています。
+それではここからは、各Textbook
モジュールの書式を、どうやって前節で説明したような、「修正前」から「修正後」の書式に移行したのか説明します。
当然、手で修正するには大変な量です。
+従来より「タイプセーフプリキュア!」ではTVシリーズ15作品に加えてキュアエコーが出てくる映画もサポートしているため、各Textbook
モジュールは16作品分存在しています。
+すでに「修正後」の書式に移行済みの「HUGっと!プリキュア」と「キラキラ☆プリキュアアラモード」を除いても、14作品分書き換えないといけません。
+シリーズごとに定義されている型やインスタンス宣言の数にはばらつきがありますが、すべて移行してから数えてみたところ、型の数だけで313個、変身や浄化技のインスタンス宣言だけで211個ありました。
+プリキュアやプリキュアに変身する女の子、変身アイテムだけでなく、それぞれの変種も別の型として定義しているため、実際のプリキュアの数よりも遙かに多いのです😵。
+Vimのマクロなどを駆使すれば決して人間の手でも移行できない規模ではありませんが、そこは「タイプセーフプリキュア!」です。
+始まって以来私がGHCの拡張を始めいろいろな技術を試すための実験場としても機能していたので、ここは是非ちょっと凝ったことをしてぱーっと書き換えてみたいものでしょう😏。
+そこで思いついたのがパーサーコンビネーター、並びに拙作のライブラリーsubstring-parserだったのです💡!
substring-parserの紹介の前に、パーサーコンビネーターについて簡単に紹介しておきましょう。
+(「すでに知ってるよ!」という方はこの節は飛ばした方が良いかと思います)
+パーサーコンビネーターは、例えば正規表現のような、文字列を解析する技術の一つです。
+Haskellのmegaparsecやattoparsecをはじめ、多くのプログラミング言語にライブラリーとして提供されています。
実装はいろいろありますが、本質的にパーサーコンビネーターは「文字列を受け取って『文字列を解析した結果』と、『残りの文字列』を返す関数」として表現されます。
+加えて、それらを簡単に組み合わせるためのAPIを提供することで、複雑な文字列から複雑なデータ構造を抽出できるようにしてくれます。
実際のパーサーコンビネーターのライブラリーを単純化して例を挙げましょう。
+例えば、通例パーサーコンビネーターのライブラリーはdecimal
という、「10進数の文字列を受け取って、整数を返すパーサー」を提供していることが多いです。
parse
関数に、解析したい文字列と一緒に渡すことで、「文字列を解析した結果」と、「残りの文字列」を取得することができます。
> parse decimal "123abc"
+123, "abc") (
👆上記の例では「解析したい文字列」として123abc
を渡したので、パースした結果の整数123
と、その残りの文字列"abc"
を返しています。
これだけではつまらないので、ほかのパーサーの例も挙げましょう。
+👇今度は「文字 セミコロン ;
を受け取って、そのまま返すパーサー」です。
> parse (char ';') ";aaa"
+';', "aaa") (
「パースした結果」がセミコロン ;
で、「残りの文字列」が"aaa"
となっていますね。
それでは以上2つのパーサーを組み合わせて、10進数の文字列を受け取った後、セミコロンを受け取り、整数を返すパーサーを作ってみましょう。
+> decimalAndSemicolon = do
+<- decimal
+ n ';'
+ char return n
+
+> parse decimalAndSemicolon "123;abc"
+123, "abc") -- 結果にセミコロンが含まれてない点に注意 (
Haskellにおけるパーサーコンビネーターのライブラリーは、パーサーをMonad
として提供することで、上記のようにdo
記法でパーサーを組み合わせることができるようになっています。
+ここでは詳細は割愛しますが、
decimal
で整数をパースしたあと、char ';'
で文字セミコロン ;
をパース(でも結果は無視)しdecimal
がパースした整数」n
を返すという処理を行っているのがわかるでしょうか?
+ちなみに、パーサーコンビネーターに慣れた読者の方なら、いわゆるApplicativeスタイルを使って、次のようにも書けると気づくでしょう。
+= decimal <* char ';' decimalAndSemicolon
これならパースした結果をいちいち変数に束縛する必要もなく、より簡潔に書くことができますね!
+パーサーコンビネーターのパワーを実感していただくために、もう一つ例を紹介します。
+many
という関数にパーサーコンビネーターを渡すと、「受け取ったパーサーコンビネーターで失敗するまで繰り返しパースして、その結果をリストとして返す」パーサーが作れます。
+例えば先ほどの「10進数の文字列を受け取った後、セミコロンを受け取り、整数を返すパーサー」から、「セミコロンが末尾に着けられた整数のリストを返すパーサー」を作ることができます。
"12;34;56;"
+ parse (many decimalAndSemicolon) 12, 34, 56], "") ([
このようにパーサーコンビネーターは、小さなパーサーをどんどん組み合わせることで、複雑な文字列から複雑なデータ構造を取り出すパーサーを、クールに作れるようにしてくれます。
+そんなパーサーコンビネーターについて、正規表現と比べた場合の長所短所を明確にしておきましょう。
+まずはよいところから。
前節で示したように、複雑なパーサーも、小さなパーサーの組み合わせからコツコツと作れるようになっています。
+さっきのdecimal
は、パースした結果を直接整数(Int
)として返していたことにお気づきでしょうか?
+正規表現で欲しい文字列からデータ構造を取り出したい際は、通常グルーピング機能を使うことになりますが、必ず一旦文字列として取り出すことになります。
+それに対してパーサーコンビネーターには、取り出した文字列を対象のデータ構造に変換する仕組みが組み込まれています。
+再帰的なパーサーを書いて再帰的なデータ構造に割り当てるのも楽ちんです。
今回の例にはありませんでしたが、例えばパースして取り出した整数の数だけ、続きの文字列を繰り返しパースする、といったことも簡単にできます。
+一方、正規表現と比べて悪いところもあります。
+正規表現はいわゆる「外部DSL」、すなわちプログラミング言語から独立した構文で提供されています。
+PerlやRubyなどの構文で言えば、/.../
の中は別世界ですよね。
+パーサーコンビネーターは、本質的に「文字列を受け取って『文字列を解析した結果』と、『残りの文字列』を返す関数」であるとおり、あくまでプログラミング言語標準の関数(のうち、文字列の解析に特化したもの)として提供されます。「内部DSL」なんて呼ばれることもあります。
そのため、正規表現とは異なり、あくまでもプログラミング言語の構文の中で使えなければならないため、使用できる文字列に限りがあり、必然的に長くなります。
+例えば先ほどのmany
は正規表現で言うところの*
(0回以上の量指定子)とちょっと似てますが、正規表現の方が3文字も短いですよね。
しかしながら、冗長であることはメリットにもなり得ます👍。
+*
をはじめ、正規表現の特殊な機能を使うには、専用の記号(メタキャラクター)をその数だけ覚えなければなりません。
+片やパーサーコンビネーターはmany
のような機能も普通の関数として提供されるため、冗長である分分かりやすい名前をつけやすいのです。
パーサーコンビネーターは先ほども触れた「内部DSL」です。
+つまり、プログラミング言語の普通の関数として使用されるものです。
+したがって、例えば正規表現をエディターの検索機能に利用すると言ったような、「ユーザーからの入力として受け取る」と言ったことは、不可能ではないものの、正規表現に比べれば難しいです。
*
にあたるmany
が、必ず強欲なマッチになるこちらについてはちょっと難しいので後述します。
+この問題は、パーサーコンビネーターをベター正規表現として使おうと思った場合に、しばしばパーサー作りを面倒くさくします。
+パーサーコンビネーターは、原理上必ず文字列の先頭から解析するよう作られています。
+例えば先ほど紹介したパーサーdecimal
の場合、
> parse decimal "abc123"
と書いても、"abc123"
は先頭が「10進数の文字列」ではないので、失敗してしまいます(実際の戻り値はライブラリーによって異なります。試してみましょう!)。
パーサーコンビネーターはそもそもの用途が0からプログラミング言語などのマシンリーダブルな構文を作るところにあるので、妥当と言えば妥当な制限です。
+その場合は必ず、文字列を頭から読んでパースすることになるでしょうから。
とはいえ、これは正規表現で例えるなら、常に先頭に\A
(あるいは ^
)を付けなければならない、あるいは自動的に付いてしまう、というような制限です。
+正規表現は行の中にある一部の文字列を抽出したり置換したりするのによく使われるので、役に立たないケースがたくさん出てきてしまいます。
パーサーコンビネーターでこの問題に対応するには、マッチさせたい文字列に到達するまで、スキップするための処理を書かないといけません。
+残念ながらこれは、正規表現で言うところの \A.*(本当にマッチさせたい文字列)
と書けばよい話ではありません。
+\A(マッチさせたくない文字列)*(本当にマッチさせたい文字列)
という書き方をしなければならないのです。
+なぜなら、先ほど触れた「正規表現でいうところの *
にあたるmany
が強欲なマッチになる」という問題があるためです。
+正規表現で言うところの\A.*(本当にマッチさせたい文字列)
を書くと、.*
が「マッチさせたくない文字列」だけでなく「本当にマッチさせたい文字列」までマッチしてしまい、結果肝心の「本当にマッチさせたい文字列」を扱うことができなくなってしまうのです。
さて、今回の目的は「『タイプセーフプリキュア!』のソースコードの書式を書き換えることで、全シリーズのプリキュアの情報をcure-index.jsonに収録する」ことでした。
+そのためには、各Textbook
モジュールのソースコードにおいて途中に含まれている、プリキュアを表す型の定義や、型クラスのインスタンス宣言を集める必要があります。
+しかもそれらは、一つの定義が行をまたいでいたりまたいでなかったりするので、よくある行単位で処理するツールを使うのも、なかなか難しいと思います。
+また、抽出したいデータ構造も多様かつそこそこに複雑で、中には再帰的なデータ構造もあります。正規表現を用いてのパースも、かなり困難なことでしょう。
+とはいえパーサーコンビネーターを通常のとおりに使うと、これまでに述べたとおり、「文字列の先頭からしかマッチできない」という制限が、考えることを複雑にします。
こうした状況は今回の問題に限らず、このように、ソースコードの多くの類似箇所を書き換える場面において、しばしば発生するでしょう。
+そこで今回は3こうした問題全般に対応するライブラリーとして、substring-parserというライブラリーを作りました。
substring-parserを使えば、任意のパーサーコンビネーター4を文字列の中間でもマッチさせることができます。
+残念ながらドキュメントらしいドキュメントが全く書けてない状況ではありますが、一応動きます。
+Spec.hsが動作を知る際の参考になるかも知れません。
substring-parserはどのようにして、任意のパーサーコンビネーターを文字列の中間でもマッチできるようにしているのでしょう?
+仕組みは単純です。
+引数として受け取ったパーサーを、
という手順を繰り返すだけです。 +結果として文字列の先頭にある「マッチさせたくない文字列」をスキップすることができるのです。
+⚠️残念ながら決して効率のいい方法ではないので、真面目なパーサーを書くときはおすすめしません!
+あくまでも今回のような、書き捨てだけど、それなりに複雑な文字列を解析する必要がある場合のみ使うべきでしょう。
ここまで説明したsubstring-parserを駆使することで、私は無事、各Textbook
モジュールを半自動で古い書式から新しい書式に書き換えることに成功しました。
+(残念ながら古いTextbook
モジュールには存在しない情報を補ったり、体裁を整えたりする必要があったため、完全に自動で書き換えられたわけではありません)
+typesafe-precure#25という大きなPull requestに、移行したもののほぼすべてが刻まれています。
なお、上記のPull requestでは消してしまってますが、実際に実行した、移行用スクリプトはtypesafe-precure/app/migrate2cure-index.hsにあります。
+ご興味のある方はご覧になってみてください。
また、もう少し小さいサンプルとして、プリキュアハッカソンの成果発表でデモをした時点のコミットも載せておきます。
+👇のコマンドを実行すれば、こちらのコミット時点のパーサーで、同時点のTypes.hsから、cure-index.jsonで使用するGirl
という型の値を取り出すことができます!
> git clone https://github.com/igrep/typesafe-precure
+> cd typesafe-precure
+> git checkout 73948fb4a82baaf4e33900d77326791c7703f786
+> stack build :migrate2cure-index
+> stack exec migrate2cure-index
+
+... 略 ...
+
+-- src/ACME/PreCure/Textbook/Dokidoki --
+Girl {girlId = "\"Mana\"", girlNameEn = "\"Mana\" ++ error \"Need family name!\"", girlNameJa = "girlName"}
+Girl {girlId = "\"Rikka\"", girlNameEn = "\"Rikka\" ++ error \"Need family name!\"", girlNameJa = "girlName"}
+Girl {girlId = "\"Alice\"", girlNameEn = "\"Alice\" ++ error \"Need family name!\"", girlNameJa = "girlName"}
+Girl {girlId = "\"Makoto\"", girlNameEn = "\"Makoto\" ++ error \"Need family name!\"", girlNameJa = "girlName"}
+Girl {girlId = "\"Aguri\"", girlNameEn = "\"Aguri\" ++ error \"Need family name!\"", girlNameJa = "girlName"}
今回は、自前で作ったライブラリーと一から書いたパーサーを組み合わせることで「ソースコードの多くの類似箇所を書き換える」問題に対応しましたが、似たようなことを行うツールはほかにもあります。
+いずれも私はほぼ使ったことがないので詳しい解説はできませんが、軽く紹介しておきます。
Facebook製の一括置換ツールです。指定したディレクトリーのファイル群を、正規表現で一括置換できます。
+ここまで書くとperl
やsed
、awk
などで十分できそうにも聞こえますが、修正前後の状態を色つきで見ながら対話的に修正できるそうです。
+正規表現での単純な修正が気に入らなければ、その場で該当箇所だけをエディタで修正できるとのこと。
+Python 2に依存しているのがちょっとつらいところでしょうか…😨。
同じくFacebookが作った、名前のとおりJavaScriptに特化したソースコードの修正ツールです。
+こちらは正規表現は使用せず、「Transform module」と呼ばれる、JavaScriptのASTを変換するための専用のスクリプトを実行することで修正するそうです。
+様々な状況に特化した「Transform module」を別パッケージとしても提供しているようです。
📝以上の2つについては「JavaScript疲れに効く! codemodとJSCodeshiftでリファクタリングが捗る - WPJ」も参考にしました。
+SuperPowers Corpという会社が開発中の、lensをはじめとするHaskellのパワーを集大成させた、ソースコードの一括置換ツールです。
+ByteString -> ByteString
という型のHaskellの関数を渡すことで、指定したディレクトリーのファイルすべてに対して関数を適用し、書き換えます。
加えて、--haskell
や--html
、--javascript
など、各言語に特化したオプションを渡すと、各言語のソースコードを修正するlensベースのmoduleをimportした状態で、関数を作れるようにしてくれます。
+具体的には、例えば--haskell
オプションを渡すと、haskell-src-extsとhaskell-src-exts-prismsパッケージのモジュールをimportすることで、HaskellのASTの各トークンに対応したPrism
などが使えるようになります。
後はbiplate
などlensライブラリーのコンビネーターと組み合わせれば、一気にHaskellのソースコードを編集することができます。
+「任意のデータ構造に対するjQuery」とも言われるlensライブラリーのパワーを存分に生かしたツールなのです。
残念なところは、今でも開発中である点と、lensライブラリーに習熟していなければ使いこなせないという点でしょうか。
+よく使うLens
型やPrism
型だけでなく、Traversal
も使えなければなりません。
+特にサンプルで紹介されているようなbiplate
を使った場合において、指定したPrism
がマッチしなかった場合、何事もなかったかのようにソースが書き換えられないため、デバッグが面倒なところもつらいです。
「タイプセーフプリキュア!」の開発は、これからもプリキュアハッカソンの前後とプリキュアAdvent Calendarの前後を中心に、今後も続ける予定です。
+先にも触れましたが、次回は今回完成させたcure-index.jsonを使用することで、かつてrubicureで作ったユナイトプリキュアを「ユナイトプリキュア」を「ディナイトプリキュア」として書き直すかも知れません。
+ただ、それ以外にももうちょっとHaskellで遊びたいことがあるので、後回しにするかも知れません。
+Vim script、あんまり書きたくないんですよね…😥
それではこの秋もパーサーコンビネーターでHappy Haskell Hacking!!✌️✌️✌️
+プリキュアハッカソンは「ハッカソン」の名を冠してはいるものの、実態としてはプリキュアの映画を観ながら好き勝手に開発するというゆるい会です。
+また、そもそもそれほど時間もないので、私は当日の3~4週間ほど前から今回の対応を始めておりました。「今回のプリキュアハッカソンに向けて行ったこと」なる見出しなのは、そのためです。↩︎
当時は各Textbook
モジュールのTypes.hs
というファイルでANN
やdeclareTransformees
などを使っていましたが、現在は「ルートに当たるモジュール」で行うことにしました。ファイル数を減らすのと、exportする識別子を型に絞ることで、transformeesHugtto
のような、あまりかっこよくない識別子を隠す、というのがその目的です。↩︎
実際には、前職時代に同様の問題に遭遇した際作成しました。今後も必要になったときにちょっとずつ開発していく予定です。↩︎
一応parsers
パッケージを使って様々なパーサーコンビネーターのライブラリーをサポートするように作りましたが、現状attoparsec
でのみテストしています。用途を考えれば多分十分じゃないかと思っています。↩︎
この記事はHaskell Advent Calendar その2兼プリキュアAdvent Calendar 20185日目の記事です。
+毎度同時投稿で失礼します。
+今年は私用で忙しかったので、のんびり書いてできあがったら空いてる日に投稿する、という楽なスタイルで書かせていただきました。なのでタイムスリップして5日目の記事と言うことにします(それにしてもずいぶん時間かかってしまってすみません、もうクリスマスも過ぎたし…😥)。
今回も例年の私のAdvent Calendarどおり、タイプセーフプリキュア!に、最近追加しようとした機能と、その際使用したもろもろの要素技術についての記事です。
+タイプセーフプリキュア!そのものについては今年9月の記事や、そこで言及しているもっと古い記事をご覧ください。
従来より、タイプセーフプリキュア!には、PreCureMonad
と呼ばれる、プリキュアの台詞をdo
記法で組み立てる機能があります。
+例えばGHCi上で下記のように書くだけで、「Go! プリンセスプリキュア」のあの名シーンを再現できます1。
> :m ACME.PreCure
+> :{
+> let scene = do
+> say "この罪を抱いたまま、もう一度、グランプリンセスを目指す!"
+> scarlet <- transform Towa (PrincessPerfume DressUpKeyScarlet)
+> scarletModeElegant <- transform scarlet (PrincessPerfume DressUpKeyPhoenix)
+> purify scarletModeElegant (ScarletViolin DressUpKeyPhoenix)
+> :}
名シーンを単純な文字列のリストとして使いたい場合はこう👇しましょう(出力は手で見やすく加工しています)。
+> composeEpisode scene
+ ghci"この罪を抱いたまま、もう一度、グランプリンセスを目指す!"
+ [ "プリキュア!プリンセスエンゲージ!"
+ , "深紅の炎のプリンセス!キュアスカーレット!"
+ , "冷たい檻に閉ざされた夢、返していただきますわ。"
+ , "お覚悟を決めなさい!"
+ , "エクスチェンジ!モードエレガント!"
+ , "スカーレット・バイオリン!フェニックス!"
+ , "羽ばたけ炎の翼!"
+ , "プリキュア! フェニックス・ブレイズ!"
+ , "ごきげんよう。"
+ , ]
さらにprintEpisode
という関数で実行すれば、1行ごとに間隔を置いてあの台詞を再生できます。
> printEpisode scene
+ ghci
+ この罪を抱いたまま、もう一度、グランプリンセスを目指す!
+ プリキュア!プリンセスエンゲージ!
+ 深紅の炎のプリンセス!キュアスカーレット!
+ 冷たい檻に閉ざされた夢、返していただきますわ。
+ お覚悟を決めなさい!
+ エクスチェンジ!モードエレガント!
+ スカーレット・バイオリン!フェニックス!
+ 羽ばたけ炎の翼!
+ プリキュア! フェニックス・ブレイズ! ごきげんよう。
そんなPreCureMonad
ですが、先ほどのコードをよく読めばわかるとおり、ちょっと不格好ですよね。
+具体的には下記の2行です。
<- transform Towa (PrincessPerfume DressUpKey_Scarlet)
+ scarlet <- transform scarlet (PrincessPerfume DressUpKeyPhoenix) scarletModeElegant
1行目のtransform
関数が、変身する女の子であるTowa
(赤城トワ)と変身アイテムを受け取ってCureScarlet
を返し、さらにそのCureScarlet
を2行目のtransform
関数に渡すことでキュアスカーレットのモード・エレガント(CureScarlet_ModeElegant
)を取得しています。
+「transform
関数が、変身する女の子であるTowa
(赤城トワ)と変身アイテムを受け取ってCureScarlet
を」返すという箇所について、Towa
に加えてCureScarlet
を新しく作っているように聞こえます。
+本来同一人物であるはずのTowa
とCureScarlet
を、あたかも別々のものとして扱っているように捉えられかねません。
+そう、本来プリキュアの「変身」は女の子自身の状態を書き換えるものとして表現した方が自然なのです。
Haskellでそうした「状態」を表現する場合、名前のとおりState
Monadを使うのが割と一般的な方法です(プログラム全体で状態を管理する場合、IORef
やTVar
などを使う方が例外に強く安全ではありますが、それはさておき)。
+しかし、従来のState
Monadでプリキュアの変身や浄化技を表現する場合、女の子が変身していない状態で浄化技(purify
)を使おうとした場合をどのように扱うか、という問題があります。
+先ほどの例で言うところの
ScarletViolin DressUpKeyPhoenix) purify scarletModeElegant (
という行でまさにその「浄化技」を実行しているのですが、プリキュアの設定上、特定の浄化技を使うには、特定のプリキュアのフォームに、専用のアイテムを渡さなければなりません。
+タイプセーフプリキュア!ではこの点に強くこだわり、浄化技が使用できる組み合わせごとに型クラスのインスタンスを定義することで、間違った組み合わせをpurify
関数に渡すと、型エラーになります(詳しくはタイプセーフプリキュア!を最初に技術的に解説した記事をご覧ください)。
+当然、まだ変身していない状態の女の子をpurify
関数に渡しても、エラーになってしまいます。
> scene = purify Towa (ScarletViolin DressUpKeyPhoenix)
+
+<interactive>:4:9: error:
+No instance for (Purification
+ • Towa (ScarletViolin DressUpKeyPhoenix))
+ of ‘purify’
+ arising from a use In the expression: purify Towa (ScarletViolin DressUpKeyPhoenix)
+ • In an equation for ‘scene’:
+ = purify Towa (ScarletViolin DressUpKeyPhoenix) scene
プリキュア実装の大先輩であるrubicureでは、同じようなケースで実行時エラーを出すようにしていますし、PreCure Monadにおいても、ExceptT
を使ってエラーにする、という方法が採れるでしょう。
+しかしそこは「タイプセーフプリキュア!」。どうにかして、変身していない状態でのpurify
関数の実行を型エラーにして、従来のこの振る舞いと一貫させたいところですよね。
+というのが今回の課題です。
今回の課題のとおり、「変身していない状態でのpurify
関数の実行を型エラー」としつつ、「変身した状態でのpurify
を型エラーとしない」ためには、purify
やtransform
を実行する前後で、State
Monad内で共有している値の型を変更できるようにする必要があります。
+残念ながら、これは従来のState
Monadでは不可能です。
+State s
に対する>>=
の型が(>>=) :: State s a -> (a -> State s b) -> State s b
となっていることから察せられるとおり、State
Monadの中で共有する型は、アクションの実行前後にかかわらず同じs
でないといけないためです。
+これはそもそも従来のMonadの仕様上やむを得ないことです。
+従来のMonadはそもそもアクションの実行前後で、アクションの実行結果以外の型を変えることができないようになっています。
+>>=
の型が(>>=) :: Monad m => m a -> (a -> m b) -> m b
となっていることからしても、アクションの実行前後でm
はm
のままであることがわかります。
この、「アクションの実行前後で、m
の型を変えることができる」ようにしたのがIndexed Monadです。
+Indexed Monadは次のような型宣言にすることで、アクションの実行前後で異なる型の “index” を挟めるようになっています。
class IxApplicative m => IxMonad m where
+ ibind :: (a -> m j k b) -> m i j a -> m i k b
IxApplicative
は名前のとおりIxMonad
と同様に”index”が付いたApplicative
となっています。詳しい定義はドキュメントをご覧ください。
唯一のメソッドであるibind
が、普通のMonadにおける>>=
の引数をひっくり返して”index”を追加したものです。
+(>>=) :: Monad m => m a -> (a -> m b) -> m b
のm
に、型引数が2つ追加されていますね?これが”index”です。
+あるIxMonad
m
がm i j a
という形で型引数を渡されている時、i
がアクションを実行する前の型、j
がアクションを実行した後の型を表します。
+a
は普通のMonad
と同様、アクションの実行結果となっています。
さらにIndexedなState
Monad (IxState
)で使えるアクションの型宣言を見れば、IxState
で共有している状態の型が、アクションの実行前後で変更できることがよりはっきりとわかるでしょう。
iget :: IxState i i i
+-- ^ igetしてもIxStateが管理している状態は変わらないため、型もやはり変わらず。
+
+iput :: j -> IxState i j ()
+-- ^ iputするとIxStateが管理している状態は、引数で渡した値の型に変わる。
こちらもおなじみmtlパッケージにあるState
Monadに、単純に “index” を加えただけのものとなっています。
Indexed Monadの世界 - モナドとわたしとコモナドで紹介された際のIndexed Monadは、ido
というQuasi Quoteを使ってdo
記法を無理矢理シミュレートしていましたが、現在はGHCのRebindableSyntax
という拡張を使うことで、普通のdo
記法をそのまま利用することができるようになりました(例は後で紹介します)。
+さらに、現在はRebindableSyntax
を使った場合の諸々の問題を回避するべく、Indexed Monadを一般化したSuper Monadと、それを簡単に使えるようにしたGHCの型チェッカープラグインが作られたり、do-notationという、Indexed Monadと普通のMonadを型クラスで抽象化したパッケージが作られたりしています。
+今回は純粋にIndexed Monadを使うだけで十分だったので、Super Monadやdo-notationは使用しませんでしたが、今後Indexed Monadをもっと実践的に使用する機会があれば、使用してみたいと思います。
Indexed Monadを使用することで、State
Monadで共有している状態の型を、アクションの実行前後で変更できるようになりました。
+続いて、各女の子の状態を、State
Monadで共有している状態の型として、どのように管理するかを検討しましょう。
+というのも、タイプセーフプリキュア!には最新のmasterの時点で59人の女の子が収録されている2のですが、それらすべてを変身前と変身後に分けて管理するだけでも、2 ^ 59通りの状態を型として表現できなければなりません。
+これを直感的に表現できるようにするために、ちょっと型レベルプログラミングの力を借りましょう。そこで登場するのが「型レベル連想配列」です。
+「型レベル連想配列」という言い方はあまりしないのでピンとこないかも知れませんが、要するに型(タイプセーフプリキュア!の場合、プリキュアに変身する女の子一人一人に個別の型を割り当てているので、その個別の型)と、それに対応する値のペアを含んだ型レベルリストです。
+大雑把に言うと、下記👇のような内容となります(実際にはもう少し違う型で構成されています)。
Hana, HasTransformed 'True)
+ [ (-- ^ プリキュアに変身する女の子を表す型(この場合「HUGっと!プリキュア」の野乃はな)
+
+Saaya, HasTransformed 'False)
+ , (-- ^ 対応する女の子が変身しているかどうかを表すsingleton type。
+-- DataKindsで型に持ち上げられたBoolを、普通の値として扱えるよう変換するためのラッパー。
+-- 申し訳なくもsingleton typeについては割愛します。
+-- Haskell-jpのSlack Workspaceあたりでリクエストがあったら書こうかな。
+
+Homare, HasTransformed 'False)
+ , (...
+ , ]
別の視点で見ると、これはいわゆるExtensible Recordとも似ています。
+extensibleパッケージやlabelsパッケージ、superrecordパッケージがそうしているように、Extensible Recordは、フィールドのラベルを表す(型レベルの、静的な)文字列をキーとして、それに対応する値を含んだ連想配列として見なすことができるためです。
+事実私は今回、extensibleを使ってこの機能を実装しました。他のExtensible Recordの実装でも良かったのですが、これ以外のものを全く使ったことがないので😅。
Indexed MonadとExtensible Recordを組み合わせることで、PreCureMonadの各種アクションを、次のように置き換えられることがわかりました。
+transform <girl> <item>
:
+IxState
(実際にはそのMonad Transformer版であるIxStateT
)で共有している型レベル連想配列のキー<girl>
に対応する値を「変身した状態」に更新する。<girl>
がすでに変身している状態の場合は、型レベル連想配列のキー<girl>
に対応する値が「変身した状態」になっているので型エラーとする。IxStateT
をかぶせたWriter
Monadで共有しているリストに、<girl>
と<item>
に対応した、変身時の台詞(文字列)を追記する。purify <precure> <item>
:
+IxStateT
で共有している型レベル連想配列のキーを取得するため、<precure>
にあらかじめ定義しておいたType Family AsGirl
を適用する。
+AsGirl
で取得した型を、これ以降<girl>
と呼びます。<girl>
が「変身した状態」になっていない場合は、型レベル連想配列のキー<girl>
に対応する値が「変身していない状態」になっているので型エラーとする。IxStateT
をかぶせたWriter
Monadで共有しているリストに、<precure>
と<item>
に対応した、浄化技を使用したときの台詞(文字列)を追記する。このように生まれ変わったPreCure Monadを✨Super PreCure Monad✨と呼ぶこととします💪
+下記がSuper PreCure Monadのサンプルコードです。
+野乃はながキュアエールに変身して、「ハート・フォー・ユー」という浄化技を放つまでを表しています。
cureYell :: PreCureM (StatusTable '[]) (StatusTable '[Hana >: HasTransformed 'True]) ()
+= do
+ cureYell Hana
+ enter Hana (PreHeart MiraiCrystalPink)
+ transform CureYell (PreHeart MiraiCrystalPink) purify
enter
は、旧PreCureMonadにはない、Super PreCure Monadに新しく追加されたアクションです。
+引数で指定された女の子や、女の子が変身したプリキュアを「登場」させます。
+具体的には、以下のように振る舞います。
<girl>
であれば、IxStateT
で共有している型レベル連想配列のキー<girl>
に対応する値を「変身していない状態」で追加する。<precure>
であれば、<precure>
にType Family AsGirl
を適用し、女の子を表す値<girl>
を取得する。
+IxStateT
で共有している型レベル連想配列のキー<girl>
に対応する値を「変身した状態」で追加する。したがって、transform
するにしてもpurify
するにしても、事前に変身前の女の子かその変身後のプリキュアがenter
していないといけません。
+これは単純にその方が実装が簡単だから、という理由もありますし、一旦「登場」させたほうがなんとなくかっこいいかな、と感じたからです。
ここまで述べたような基本的な仕様は実装できたものの、まだ解決すべき技術的な問題が見つかったので、残念ながらリリースはされていません(その詳細は気が向いたら書きます)。
+なので、試す場合は下記のように実行してください。
$ chcp 65001
+-- ^ Windowsの方は恐らく必要
+
+$ git clone -b super-precure-monad https://github.com/igrep/typesafe-precure.git
+$ cd typesafe-precure
+$ stack build
+$ stack exec ghci
+> :set -XRebindableSyntax -XFlexibleContexts -XTypeFamilies
+> import Prelude hiding ((>>), (>>=))
+> :m + ACME.PreCure ACME.PreCure.Monad.Super
+> :{
+> scene = do
+> enter Makoto
+> transform Makoto (LovelyCommuneDavi CureLoveads)
+> purify CureSword (LovelyCommuneDavi CureLoveads)
+> :}
+> printEpisode scene
+
+ (ダビィー!)
+ プリキュア!ラブリンク!L! O! V! E!)
+ (
+ 勇気の刃! キュアソード!
+ このキュアソードが 愛の剣で、あなたの野望を断ち切ってみせる! 閃け!ホーリー・ソード!
「変身していない状態でのpurify
関数の実行を型エラーとする」といった仕様を試す場合は、こちらに置いた、全プリキュアの変身と浄化技を列挙したテスト用ファイルをghciで読んでみるといいでしょう。
+先ほど👆の手順でgit clone
したディレクトリーにおいて、あらかじめstack build
を実行しておくのをお忘れなく。
$ stack build
+$ stack exec ghci gen/AllPreCureM.hs
+適当にgen/AllPreCureM.hs
を書き換えて:r
してみれば、概ねいい感じに動いていることがわかるはずです。
例えば冒頭付近にある、
+= printEpisode $ do
+ act_CureDiamond_LovelyCommuneRaquel_CureLoveads Rikka
+ enter Rikka (LovelyCommuneRaquel CureLoveads)
+ transform CureDiamond (LovelyCommuneRaquel CureLoveads) purify
というSuper PreCure Monadによるアクションから、transform Rikka (LovelyCommuneRaquel CureLoveads)
という行を削除した上で:r
してみると、次のようなエラーになります。
> :r
+[1 of 1] Compiling AllPreCureM ( gen\AllPreCureM.hs, interpreted )
+
+gen\AllPreCureM.hs:22:3: error:
+ • Couldn't match type ‘'False’ with ‘'True’
+ arising from a use of ‘purify’
+ • In a stmt of a 'do' block:
+ purify CureDiamond (LovelyCommuneRaquel CureLoveads)
+ In the second argument of ‘($)’, namely
+ ‘do enter Rikka
+ purify CureDiamond (LovelyCommuneRaquel CureLoveads)’
+ In the expression:
+ printEpisode
+ $ do enter Rikka
+ purify CureDiamond (LovelyCommuneRaquel CureLoveads)
+ |
+22 | purify CureDiamond (LovelyCommuneRaquel CureLoveads)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Failed, no modules loaded.
+ちゃんと、変身していない状態でpurify
することを型エラーにできていますね!
ここまでできていながら残念ですが、リリースは、来年のプリキュアハッカソンかAdvent Calendarあたりに乞うご期待と言うことで!💦
+それでは2019年もHaskellでSuper PreCure Hackingを❣️❣️❣️
こんにちはkakkun61こと岡本和樹です。
+来たる10月8日(月・祝)に池袋にて技術系同人誌即売会「技術書典5」が開催されます。
+Haskellを題材にした同人誌もいくつかあるようですのでまとめてみました。
+(バナー画像は技術書典5サイトよりの引用です。)
+技術書典とは技術系同人誌即売会としておそらく日本最大のもので、今回で6回目の開催となります。(ニコニコ超会議内での超技術書典があったのでナンバリングがずれています。)
+ +これまでは秋葉原での開催でしたが、今回はなんと場所を3倍の広さに拡張して池袋で開催されます。
+それにともないサークル数もどどんと470超となり、1サークル45秒で回っても全サークルは見て回れないことになります。
+事前準備の重要性が高まった今回、Haskeller向けにHaskellサークルをまとめてみました。
+++ ++あと技術書典5に、鴨川書房というサークル名で合同本を出品します。HaskellによるNN実装(@lotz84_) や、FPGAでのauto encoder実装に関する苦労話等が掲載される予定です。ぜひに🙏…… +
+— ✨🤩😝🤪パリピ🤭🤢🤮✨ (@chaoticCats) 2018年8月9日 +
++ ++形態素解析ライブラリnagisaについては,技術書典の合同本に寄稿してもらえるよう作成者に依頼中なので興味ある方は是非(表紙は鋭意作成中)。 pic.twitter.com/zUtngAS23t +
+— ✨🤩😝🤪パリピ🤭🤢🤮✨ (@chaoticCats) 2018年9月18日 +
++代数的数(整数係数多項式の根として表される数)を実装するためのアルゴリズムを解説します。代数的数を使うと、ルートを含むような数に関して、浮動小数点数の誤差に煩わされることなく正確な演算が行えます。Haskellによるサンプルコードを掲載しています。
+この本は、Web連載していた「週刊 代数的実数を作る」 https://miz-ar.info/math/algebraic-real/ の書籍化です。本文の加筆修正の他、「付録A ユークリッドの互除法と拡張された互除法」「付録B 部分分数分解」を追加しています。
+カタログより
+
++ ++技術書典5 か38で「代数的数を作る 多項式の根と因数分解のアルゴリズム」を頒布します。よろしくお願いします。 https://t.co/HkLF1YFDuN pic.twitter.com/V17ZIj2Iub +
+— だめぽラボ@技術書典5 か38 (@mod_poppo) 2018年9月29日 +
私のサークルです。新刊落としました……
+++Haskellでの手続きプログラミングの側面について解説します。
+対象読者 +- Haskell入門書程度が読める +- 特に読めるが書こうとすると悩む人に読んでほしいです +- 手続きプログラミングのプログラマー +- 厳密に本書を読むためにはHaskellを読めた方がよいですが、手続きプログラミングですのでプログラマーなら雰囲気で読めると思います
+書かれてあること +- 書き換え可能な変数 +- 手続きプログラミング的な制御構造 +- 配列 +- サンプルプログラム +- 手続き的な実装とHaskell的な実装の対比
+電子版(PDF)はこちらで販売中です。 +https://kakkun61.booth.pm/items/829369
+カタログより
+
++【HaskellのウェブアプリケーションフレームワークYesodの入門書!】
+本書は、Haskellの入門書レベルの知識をもつ読者を対象とした、ウェブアプリケーションフレームワークYesodの入門書です。比較的学習コストの高いYesodですが、本書を通じてYesodの基本的な知識とHaskellでのウェブアプリケーション開発に挑んで見ましょう! +〈本書の対象読者〉 +Haskellの入門書は既に読みこなしているプログラマ +Haskellでウェブアプリを作ってみたいプログラマ
+出版社ページより
+
ちなみにこんな本を作るつもりでした。欲しい方いらっしゃったら次で書けとお伝えください。はげみになります。
+++ ++技術書典5にサークル「趣味はデバッグ……」として参加申込をしました! | 技術書典 https://t.co/nD4eBo9622 「自作静的型付け言語を作ってそれに対して型推論する方法を解説する」書籍を作るぞ! +
+— kakkun61@技術書典5 か61 (@kakkun61) 2018年6月20日 +
++ゆるふわにこまき数学!
+以下のような人に向けて、頒布します。
++
+- 数学・代数の雰囲気をゆるく知りたい
+- 軽いHaskellを知りたい
+- なんでもいいから技術系にこまきが読みたい
+カタログより
+
++ ++技術書典5の「か74」で、矢澤にこ先輩が簡単なHaskellで代数(半群・モノイド・群・環・体)を教えてくれる本を頒布します。
+— あいや🤘🙄🤘技術書典5@か74 (@public_ai000ya) 2018年9月29日 +
よろしくお願いします🐕https://t.co/KBFxqX69m3
☝サークルページ#技術書典 #技術書典5 pic.twitter.com/HvD5ql4gFl +
当日は安全に配慮しつつ楽しんでいきましょう!!
+1000円札と500円玉の準備はしっかりとね。
+Haskell-jp Blogでは、設立当初よりHaskellに関する記事を幅広く募集してきました。
+このたびはそれに加え、このHaskell-jp Blogで「書いて欲しい!」「読んでみたい!」Haskellに関する話題も募集することにしました!
+例えば、下記のような話題が考えられるでしょう。
ただし、提案していただいたネタに関する知見の持ち主が居ないかもしれませんし、誰かの負担になるものなので必ず記事になるとは限りません。 +また、場合によっては既に記事があるため、既存の記事を薦められるかもしれません。
+このBlogのリポジトリのIssueからお願いします。 +既に提案されていたら、そのIssueに対して 👍 するといいと思います。
+TopicRequest
というラベルを作ったので、Issueをそのラベルで検索してください。
+書いてもいいという提案があった場合は、Issueに「書いてもいいよ」という旨をコメントしていただけるとバッティングが無くて助かります。
それでは、今後はHaskellに関するあなたの記事だけでなく、あなたがHaskellについて読みたい記事も募集していきますので、どしどし応募してください! 🙏
+あらゆるソフトウェアに脆弱性は存在し得ます。
+Haskellは高度な型システムを駆使することで、脆弱性を根本的に回避したプログラムを作ることを可能にします(脆弱性を防ぐためだけのものではないですが、興味のある人はSafe Haskellについても調べてみるといいでしょう)。
+しかし、だからといって、型を設計する段階で脆弱性を回避できるよう気をつけなければいけないことには変わりませんし、GHCが生成した実行ファイル、使用するライブラリーに絶対に脆弱性がないとは言えません。
+現状、Haskellはほかの著名なプログラミング言語ほど使用されていないためか、あまり脆弱性が報告されることはありません(libcなど、ほかの言語の処理系も依存しているようなライブラリーの脆弱性は別として)。
+今回は、そんな中でもunordered-containersというパッケージについて、ドキュメントにも書かれているためおそらく直ることがないであろう脆弱性と、その回避方法について紹介します。
+hashdos脆弱性自体は結構有名ですし、ドキュメントに書いてあることなので、ご存知の方には何を今更感があるかと思いますが、検索した限りこの問題について日本語で説明した記事は見当たらなかったので、ここで紹介します。
脆弱性の前にunordered-containersパッケージについて簡単に紹介しましょう。
+unordered-containersパッケージは、GHCに標準で付いているcontainersパッケージよりも高速な連想配列(HashMap
型)や集合(HashSet
型)を提供してくれます。
+StackageのLTS Haskell 10.3ではなんと970ものパッケージに依存されている、超大人気汎用パッケージです。
HashMap
という名前が示しているとおり、キーとなる値のハッシュ値を計算・利用することで高速化しています。
+しかし、Java言語などほかの言語によくあるHashMap
とは大きく異なり、内部ではハッシュテーブルを使用していません。
+本物のプログラマはHaskellを使う - 第35回 キーを使って値を参照するMap型:ITproでも説明しているとおり、ハッシュテーブルはミュータブルな配列を内部で使用していることから、イミュータブルなデータ構造を使用して行う関数型プログラミングとは、相性が悪いのです(ST
モナドやIO
モナドを利用したhashtablesパッケージなどを使えば、限られた範囲内でハッシュテーブルを使うこともできます)。
ハッシュテーブルを使用しない代わりに、unordered-containersでは内部でHash array mapped trieという特殊な木を使っています。
+どのような構造かは、HAMT ~ イミュータブルで高速なハッシュマップ ~ | κeenのHappy Hacκing Blogに詳しく書かれています。
+こちらのスライドはScalaでの実装の話ですが、基本的にはunordered-containersパッケージのHashMap
も同じはずです。
大雑把に言うと、Hash array mapped trieを使ったHashMap
では、ハッシュテーブルと同様に、キーとなる値をハッシュ関数で一旦固定長の整数に変換することで、キーが存在しているかどうかの確認を高速化しています。そのため、containersパッケージよりも高速な処理ができるのです。
+containersパッケージのMap
ではキーの存在を確認する際、キー全体を既存のキーと比較する必要があるため、特に長い文字列をキーとする場合は、処理が遅くなりがちだったのです。
hashdos脆弱性は2011年頃RubyやPHP、Perlなど多くのプログラミング言語が影響を受けるとされた、著名な脆弱性です。
+ここでも簡単に仕組みを説明しましょう。
前節で説明したとおり、Hash array mapped trieもハッシュテーブルも、必ずキーを一旦固定長の整数に変換します。
+文字列など、ハッシュ関数を適用されるキーとなる値は、当然固定長の整数よりも幅広い値を取り得るので、違う文字列同士でも、同じハッシュ値をとることがあります。
+この、違う値であるはずのキーが同じハッシュ値をとってしまった状態を「ハッシュ値の衝突」と呼びます。
+ハッシュ値の衝突が発生した場合、ハッシュテーブルやHash array mapped trieといったハッシュ値を利用した連想配列は、(単純な)配列やリストなど、やむを得ず逐次探索が必要なデータ構造を内部で使用しなければならなくなります。
hashdos脆弱性はこの性質を利用したDoS攻撃です。
+攻撃者は、あらかじめ対象のプログラムで使っているハッシュ関数が、「必ず同じハッシュ値」を返すキー(大抵文字列でしょう)を大量に用意して、それを対象のプログラムに入力として与えることで、簡単にDoS攻撃を仕掛けることができるのです。
+先ほど触れた徳丸先生の記事では、PHPのアプリケーションに対してわずか500KBのform-dataを送るだけでCPU時間を1分も消費させることができたそうですから、その威力はすさまじいものと言えるでしょう。
unordered-containersのDeveloper Guideには、次のように書かれています。
+++There’s an uncomfortable trade-off with regards to security threats posed by e.g. denial of service attacks. Always using more secure hash function, like SipHash, would provide security by default. However, those functions would make the performance of the data structures no better than that of ordered containers, which defeats the purpose of this package.
+
要するに、「セキュリティー上問題はあるけど、SipHashのような安全なハッシュ関数を使ったらcontainersパッケージよりも速度が出なかった。それではこのパッケージの意味がない」ということです。
+containersパッケージよりも高速な連想配列を作るためにunordered-containersパッケージを作ったのだから、それより遅くなっては存在価値がなくなってしまうのです。
+従って、ユーザーが任意にキーを入力できるようなプログラムでは、unordered-containersではなく、containersを使え、ということです。
+このことはunordered-containersが使用しているhashableのドキュメントにも書かれています。ある意味ノーガード戦法ですね。
前節で触れたとおりですが、ユーザーが任意にキーを入力できるようなプログラムでは、unordered-containersパッケージのHashMap
やHashSet
ではなく、containersパッケージのMap
やSet
を使いましょう。
+containersパッケージにあるMap
やSet
はハッシュ関数を一切使っていないので、ハッシュ値の衝突も起こらず、内部で逐次探索が必要なデータ構造を使ってもいません。
+なのでhashdos攻撃に遭うことはないのです。
ただし、実際のところ、StackageのLTS Haskell 10.3で970ものパッケージに依存されているunordered-containersです。
+その中にはJSONのパーサーであるaesonも含まれているので、もしかしたら現状回避するのは非常に困難なのかもしれません。😱
+次回は、この問題について試しに攻撃用のコードを書いて速度の低下をチェックして報告する話を書くかもしれません…。😰
2022/10/23 追記: aesonパッケージがHashDoS脆弱性を孕んでいるという問題は、2021年9月に発表され、同脆弱性はaesonパッケージのv2.0.1.0で修正されました。現在は、コンパイル時のフラグを編集しない限り、内部でcontainersパッケージのMap
を使うようになりました。ただし、フラグのデフォルト値は将来変更するかも知れない、と開発者が明言しているので、心配な方はcabal.projectやstack.yamlでフラグの値を指定しましょう(参考)。この記事を書いた時点で当然私もこの問題には気づいていたのですが、再現ケースの作成に失敗したこともあり、報告に至らなかったことを反省します。本件を発表した記事と同じ、fnv-colliderを使ったはずなのになぁ😞。
This is the English version of WindowsでHaskellを扱う時によく遭遇するエラーと対処法.
+The original article is the 4th article of Haskell (その4) Advent Calendar 2017 (Japanese).
What I’m going to tell is summarized as just one tweet (originally in Japanese):
+++What I’ve learned:
++
+- chcp65001 if ‘Invalid character’
+- rebuild if ‘Permission Denied’
+- Don’t mix Japanese characters in file paths.
+- Some libraries in C are available, and others are not.
+Perhaps they’re helpful in other languages.
+
Let me add more details.
+You would have encountered this frequently, especially if you don’t know how to avoid/fix this.
+Oh, it’s caused again by building with hakyll!
> stack exec -- site rebuild
+...
+ [ERROR] preprocessed-site\posts/2017/01-first.md: hGetContents: invalid argument (invalid byte sequence)
+The object called Handle
, used by GHC to read and write a file, knows its character encoding.
This resembles Ruby’s IO
and Perl’s file handler.
+Both of them represent the “gateway” of data, and assigning character encoding to them enables us to handle the only, consistently encoded strings by converting the incoming data.
+In Haskell’s type Char
, the only default encoding is UTF-32 (is this the right name in this case?).
The character encoding assigned to a Handle
by default depends on the locale settings of the OS: in Japanese Windows, Windows-31J (a.k.a CP932).
+But it’s now soon becoming 2018 (when writing the original article). Most files you create should be in UTF-8 unless you write programs in notepad.exe1.
+It doesn’t work to read a UTF-8 file as a Windows-31J file because they’re very different encoding system.
+The invalid byte sequence
error, shown at the head of this section, is caused by that inconsistency.
+Remember this kind of errors are often caused when reading or writing stdout/stdin, as well as plain files.
In many cases you can avoid these kind of errors by running the below command in advance.
+> chcp 65001
+> stack exec -- site rebuild
+... Should work!
+This command temporarily changes the character encoding in the current Command Prompt session.
+The number 65001
seems to stand for UTF-8.
+To roll it back, run chcp 932
.
> chcp 932
+It seems that the “932” of “CP932” is the same “932” entered here!
+The chcp
command is available in MSYS2’s bash (Surprises me a little. Wondering how it works…😕).
+But you should know that chcp
exists at C:\Windows\System32\
, which MSYS2 users usually don’t want to include in the PATH
.
+The directory contains many incompatible commands whose names conflict with the tools loved by Unix people (e.g. find.exe
)!
So I’ve dropped C:\Windows\System32\
from PATH
when using MSYS2.
+If you’ve done like me, run by full path:
/c/Windows/System32/chcp.com 932
+Unfortunately, the error can often persist even after running chcp 65001
2.
+According to my guess, the chcp 65001
command doesn’t affect the grandchild processes of the Command Prompt (or bash etc.) on which the chcp
is run (i.e. the child processes of the command you enter).
If the error still happens you can either report to the developer, or fix it yourself!
+When reporting; asking the developer to run after doing chcp 932
could help him/her reproduce the bug (Sorry, I’ve never tried it).
+When fixing by yourself, perhaps the best and most certain way would be to switch the character encoding of the Handle
object.
This problem is caused by the inconsistency between the Handle
's character encoding and the encoding of the bytes that are actually transferred. So switching into the proper encoding should fix it.
+If the error happens when reading/writing a common UTF-8 file via the Handle
, writing like below can avoid it:
import System.IO (hSetEncoding)
+import GHC.IO.Encoding (utf8)
+
+ hSetEncoding handle utf8
As a bonus, I’ll show you an example of how I myself addressed a problem caused by the standard output (or standard error output), and fixed a bug in haddock.
+In short, it can at least suppress the error to paste the code below before your program uses the Handle
(Copied from this commit).
{-# LANGUAGE CPP #-}
+
+import System.IO (hSetEncoding, stdout)
+
+#if defined(mingw32_HOST_OS)
+import GHC.IO.Encoding.CodePage (mkLocaleEncoding)
+import GHC.IO.Encoding.Failure (CodingFailureMode(TransliterateCodingFailure))
+#endif
+
+...
+
+#if defined(mingw32_HOST_OS)
+$ hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure
+ liftIO #endif
CPP macros to import
modules only available on Windows makes this code hard to read, so let’s cut out the verbose part:
hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure
+Here’re the details:
+First of all, hSetEncoding
is the function to change the Handle
‘s character encoding, as I referred before.
+Then stdout
is the Handle
for the standard output as its name.
+The last function call mkLocaleEncoding TransliterateCodingFailure
returns a character encoding object for the current Windows’ character encoding (i.e. chcp
ed character encoding), configured as “Even if the Handle
detects any characters which can’t be converted into/from a Unicode character, don’t raise an error, convert it into some likable character instead.”.
As the result of the hSetEncoding
above, and the current character encoding is Windows-31J, the character used in the compilation error of GHC:
↓This character
+• No instance for (Transformation Nagisa CardCommune_Mepple)
+↑
+is converted into
+? No instance for (Transformation Nagisa CardCommune_Mepple)
+the question mark. Yeah, this is the “?” I bet most users of GHC on Japanese Windows have seen at least once 😅
+This makes me guess GHC executes hSetEncoding stderr $ mkLocaleEncoding TransliterateCodingFailure
by default before printing out the compilation error.
+Anyway, it’s good that the program doesn’t abort due to the error!
As the last note of this section: Read the document of GHC.IO.Encoding for the details of how GHC handles various character encodings.
+I’ve made the first section too long for “Quick-and-dirty checklist”, but I’ll tell you in short from this section.
+We often encounter some errors like “Permission Denied”, “Directory not empty” and similar ones when running stack build
, ghc
, elm-make
, and any other commands written in Haskell.
+To tell the truth, I’m completely not sure of the cause, but those errors disappear by running the same command several times.
+The key is to repeat many times. Never give up only by once or twice 😅
+Turning off your antivirus software’s scanning of the problematic directory, Dropbox’s synchronisation, etc. might also fix such errors.
On Windows, it frequently troubles us to install libraries which depend on libraries written in C (registered as lib***
in your OS’s package manager).
+But this is not the case only for Haskell.
The way to fix depends on the case, so let me give you some examples as external links (Sorry, all pages are written in Japanese!).
+That’s all!
+Then, Happy Hacking in Haskell on Windows 10!! I don’t know WSL!🏁🏁🏁
Translator’s note: In Japanese locale, notepad.exe saves the file in Windows-31J. This will be changed (into UTF-8) in the future release of Windows 10.↩︎
By the way, when I once tried to build the compiler of Eta, (as far as I remember) chcp 65001
didn’t fix the problem, but chcp 20127
did.
+As chcp 20127
switches into US-ASCII, I suspect the local environment of the developer of Eta is US-ASCII…↩︎
去年、WindowsでHaskellを扱う時によく遭遇するエラーと対処法という記事で、WindowsユーザーがHaskellで開発したとき、あるいはHaskell製のプログラムを使用した際によく遭遇するエラーやその回避方法を紹介しました。
+今回は、そこに追記したい内容として、最近私がよく出遭うようになったエラーを紹介します。
openFile: does not exist (No such file or directory)
といわれたら短いパスに移そうdoes not exist (No such file or directory)
というエラーは、本当に読んで字のごとく、開こうとしたファイルが存在しないためのエラーであることとがもちろん多いのですが、エラーメッセージに反して違う原因である場合もあります。
例えば、最近私はとあるプロジェクトを数文字長い名前にリネームしたのですが、たったそれだけで、stack test
した際必ず問題のエラーが発生するようになってしまいました。
$ stack test
+a-little-longer-name-project-0.1.0.0: build (lib + exe + test)
+Preprocessing library for a-little-longer-name-project-0.1.0.0..
+Building library for a-little-longer-name-project-0.1.0.0..
+Preprocessing executable 'mmlh' for a-little-longer-name-project-0.1.0.0..
+Building executable 'mmlh' for a-little-longer-name-project-0.1.0.0..
+Preprocessing test suite 'a-little-longer-name-project-test' for a-little-longer-name-project-0.1.0.0..
+Building test suite 'a-little-longer-name-project-test' for a-little-longer-name-project-0.1.0.0..
+[1 of 5] Compiling Paths_aLittleLongerNameProject ( .stack-work\dist\5c8418a7\build\a-little-longer-name-project-test\autogen\Paths_aLittleLongerNameProject.hs, .stack-work\dist\5c8418a7\build\a-little-longer-name-project-test\a-little-longer-name-project-test-tmp\Paths_aLittleLongerNameProject.o )
+.stack-work\dist\5c8418a7\build\a-little-longer-name-project-test\a-little-longer-name-project-test-tmp\.stack-work\dist\5c8418a7\build\a-little-longer-name-project-test\autogen\Paths_aLittleLongerNameProject.dump-hi: openFile: does not exist (No such file or directory)
+どういうことかと悩んでいたところ、こんなIssueを見つけました。
+Snoymanの指摘のとおり、こちらの問題はWindowsで使えるパスの長さが原因のエラーのようです。
+どういうことかというと、MSDNのこちらのページでも触れているとおり、Windowsの(C言語レベルでの)各種ファイル操作用APIでは、一度に扱えるパスの長さが260文字までと決められていて、その制限にかかったためのエラーだというのです!
+does not exist (No such file or directory)
なんてエラーメッセージで表されるのでわかりづらい!(おそらくWindowsのエラーコードの出し方に問題があるんじゃないかと思います)
DOS時代から残るこの制限、完全に時代錯誤なものでしかないのですが、Windowsでパッケージマネージャーなどが自動的に作ったパスを扱っていると、しばしば出くわすことがあります。
+stackにおいても、こちらのIssueで同じ問題が議論されていたり、ver. 1.6.5のChangeLogでも言及されていたりと、至る所で格闘している跡があります。
そんなdoes not exist (No such file or directory)
ですが、残念ながら私が知る限り、プロジェクトなどのパスを(C:\
などのよりルートに近い場所に置いて)より短くする以外の回避方法はありません。
+haskell-ide-engineのインストール方法のページ曰く、(新しめの)Windows 10であれば、グループポリシーを編集して、「Win32の長いパスを有効にする」を「有効」にすれば回避できるとのことですが、残念ながら手元で試した限りうまくいきませんでした。何かやり方がまずかったのかもしれませんが。
+いずれにしても、stack build
コマンドなどを実行したときに問題のエラーに遭遇した場合、ビルドしたいもののパスをなんとかして短くする以上の方法はありません。
+C:\
直下をホームディレクトリのように使う人が今でもたくさんいるわけです。
一方、あなたが問題のエラーが発生するプログラムを修正することができる立場にある場合、次の方法で回避できるかもしれません。
+本件はあくまでも、Windowsの各種ファイル操作用APIの1回の呼び出しで渡せる長さの制限ですので、制限を超えてしまうような場合はパスを分割すればよいのです。
+filepathパッケージのsplitFileName
関数やsplitPath
関数を駆使してパスを分割した上で、対象のファイルの親ディレクトリーまでdirectoryパッケージのsetCurrentDirectory
関数で移動すれば、制限に引っかからないはずです(時間の都合でこちらについては試すコードを用意しておりません。あしからず)。
残念ながらカレントディレクトリーはプロセス全体で共有される情報ですので、マルチスレッドなプログラムでは頭の痛い問題が出てきてしまいますが、一番確実に回避できる方法のはずです。
+マルチスレッドである場合を考慮したくない場合は、次に紹介する方法を検討するとよいでしょう。
\\?\
というプレフィックスを着けた絶対パスを渡す。ここまでに出てきた、「Windowsの各種ファイル操作用API」は、すべて「Win32 API」と呼ばれるWindows固有のAPI群の一部です。
+この「Win32 API」に含まれる関数の多くは、「ユニコード版」とそうでないものに分かれます(詳細はConventions for Function Prototypes (Windows)をご覧ください)。
このうち、「ユニコード版」のAPIには、この制限を緩和する専用の機能が含まれています。
+先ほども触れたMSDNのページ曰く、なんと\\?\
という変な文字列を絶対パスの頭に着けると、最大約32,767文字のパスまで受け付けるようになるというのです!
+なんともアドホックな感じのする解決方法ですが、Microsoftが言うんだから間違いありません。
+いずれにしても32,767文字という微妙な最大文字数ができてしまいますが、UTF-16での32,767文字なので、そう簡単に超えることはないでしょう。
+いちいち絶対パスに変えて変なプレフィックスを加えないといけないという面倒くささはありますが、いちいち分割して相対パスに変換するよりは簡単なはずですので、検討する価値があります。
この、\\?\
機能を試す場合、下記のコードを適当なファイルに貼り付けて保存し、stack runghc file.hs
などと実行してみてください (Thanks, @matsubara0507!)。
+catch
関数を使って例外を捕捉している箇所では、実際にパスが長すぎるためにエラーが発生し、catch
されているはずです。
import Control.Exception (catch, IOException)
+import Data.List (replicate)
+import System.Directory
+
+main :: IO ()
+= do
+ main <- getCurrentDirectory
+ crDir let
+ = mconcat $ replicate 20 "abcdefgh/" -- ok
+ path1 = mconcat $ replicate 30 "abcdefgh/" -- error
+ path2 = crDir ++ "/" ++ path2 -- error
+ path3 = "\\\\?\\" ++ path3 -- ok
+ path4
+putStrLn $ "path1: " ++ show path1
+ True path1
+ createDirectoryIfMissing
+putStrLn $ "path2: " ++ show path2
+ True path2 `catch` (\e -> putStrLn $ " " ++ show (e :: IOException))
+ createDirectoryIfMissing
+putStrLn $ "path3: " ++ show path3
+ True path3 `catch` (\e -> putStrLn $ " " ++ show (e :: IOException))
+ createDirectoryIfMissing
+putStrLn $ "path4: " ++ show path4
+ True path4 createDirectoryIfMissing
さて、またしてもWindows固有の面倒な問題を紹介することとなってしまいましたが、俗世の喜び(主にゲーム)と簡単にインストールできるGUIに慣らされてしまった私は、今後もWindowsを使い続けるつもりです。
+いろいろ困難は尽きませんがこれからもWindowsでHappy Haskell Lifeを!🏁🏁🏁
※本文中で言及していないもののみ
+ +先日、Emscripten & WebAssembly night !! #7というイベントにて、AsteriusというHaskellをWebAssemblyにコンパイルするツールについて紹介いたしました。
+資料はこちら👇です。
AsteriusでHaskellの関数をJSから呼べるようにしてみた(けど失敗)
+本日は、スライドの英語で書いていた箇所を和訳しつつ、いろいろ捕捉してブログ記事の形で共有します。
+冒頭でも触れたとおり、AsteriusはHaskellのソースをWebAssemblyにコンパイルするコンパイラーです。
+GHCのHEAD(開発中のバージョン)を都度フォークして、現在活発に開発中です。
+Template Haskellと、GHC標準におけるIOを行う関数(の大半)を除いた、すべての機能が利用できるようになっています。
+現状のWebAssemblyを実用する上で必要不可欠であろう、FFIもサポートされています。
+つまり、JavaScriptからWebAssemblyにコンパイルされたHaskellの関数を呼んだり、HaskellからJavaScriptの関数を呼ぶことができます!
+何かしらのIO処理を行う場合は、基本的にこのFFIを使ってJavaScriptの関数を呼ぶことになります。
加えて、ahc-cabal
という名前のコマンドで、cabalパッケージを利用することもできます。
+こちらはcabal
コマンドの単純なラッパーです。ahc-cabal new-build
などと実行すれば、外部のパッケージに依存したアプリケーションも、まとめてWebAssemblyにコンパイルできます。
+本格的に開発する上では欠かせないツールでしょう。
Asteriusは、“A linker which performs aggressive dead-code elimination, producing as small WebAssembly binary as possible.”と謳っているとおり、GHCのランタイムを抱えているにしては、比較的小さいWASMファイルを生成するそうです。
+というわけで手元で試してみたところ、下記のような結果になりました。
main = return ()
しかしないソース):
+.wasm
ファイルのみ)。なかなかいい感じですね。.mjs
ファイルを含めた合計)。未圧縮でこれなら確かに十分軽いでしょう。Webpackなどで結合・minifyするともっと軽くできますし。.wasm
ファイルのみ)。うーん、ちょっと苦しいような…😥。.mjs
ファイルを含めた合計)。.mjs
ファイルの内容は特に変わりませんでした。ちなみに、移植前の元のソースを含むアプリを、Linux 64bit向けのELFファイルとしてビルドして比較してみたところ、.wasm
ファイルよりも少し小さいぐらいでした。
+詳細な内訳が気にはなりますが、今のソースですと大体これぐらいが限界なのかも知れません(でもWASMは現状32bitバイナリー相当のはずだし、もう少し小さくならないものか…)。
加えて、Asteriusを利用して開発すると、ほぼ最新のGHCの開発版が使える、というところも、新しもの好きなHaskellerをわくわくさせるところですね!(今回はあいにく新しい機能について調べる余裕もなかったので、特に恩恵は受けてませんが…😅)
+Asteriusは、GHCをフォークしていくつかの機能を追加して作られているものです。
+しかし幸いオリジナルとの差分が十分に小さく、作者が定期的にrebaseすることができています。
+詳細な違いはAbout the custom GHC forkにまとまっています。近い将来GHC本体に取り込まれそうな修正ばかりではないかと。
それからこれは、ブラウザーでHaskellを動かすことができるという点でAsteriusの競合に当たる、GHCJSと比較した場合の話ですが、FFIを利用して、JavaScriptから直接Haskellを呼ぶことができるようになっているのも、優れた点と言えるでしょう。
+GHCJSはこちらのドキュメント曰く、JavaScriptからHaskellを呼ぶ機能は備えてはいるものの、簡単ではないためドキュメントも書かれておらず、推奨されていません。
+これでは状況によってはかなり使いづらいでしょう。
+今回私が試したように、コアとなる処理だけをHaskellの関数として書いて、それをJavaScriptから呼び出すということができないのです。
一方Asteriusでは、例えば👇のように書くことで、WASMがエクスポートする関数として、func
をJavaScriptから呼べるようにすることができます!
"func" func :: Int -> Int -> Int foreign export javascript
ただし、実際に今回試してみたところ、Asteriusではまだバグがあったので、この用途では依然使いにくいという状況ではありますが…(詳細は後で触れます)。
+Asteriusは、やっぱりまだまだ開発中で、バグが多いです。
+今回の目的もバグのために果たせませんでした😢。
先ほども触れたとおり、特に未完成なのが、IOとTemplate Haskellです。
+GHCなら使えるはずのIO
な関数の多くが使えませんし、Template Haskellに至っては一切利用できません。
IOについては、現状、(putStrLn
などのよく使われる)一部を除き、FFI(foreign import javascript
)を使ってJavaScriptの関数経由でよばなけれなりません。
+これは、入出力関連のAPIを一切持たないという現状のWebAssemblyの事情を考えれば、致し方ない仕様だとも言えます。
+WASIの策定によってこの辺の事情が変わるまでの間に、すべてforeign import javascript
で賄うというのも、なかなか面倒なことでしょうし。
Template Haskellに関しては、現在こちらのブランチで開発中です。…と、思ったらこのPull request、Closeされてますね…。
+これに関して詳しい事情はわかりません。いずれにしても、Template Haskellを実装するには、コンパイル時にその場でHaskellを評価するためのインタープリターが別途必要だったりして、結構ハードルが高いのです。
加えて、RTS(この場合、コンパイルしたHaskellを動かすのに必要なWASMやJavaScriptファイル)がBigInt
に依存している関係で、V8やSpiderMonkeyでないと動かない点もまだまだ、という感じです。
+ブラウザーで言うと、2019年5月3日時点でChromeか、FirefoxのBeta版以降でないと使用できません1。
Asteriusのドキュメント「IR types and transformation passes」をざっくり要約してみると、Asteriusは以下のような流れで動くそうです。
+実際にはahc-link
というコマンドがこれらの手順をまとめて実行するので、ユーザーの皆さんはあまり意識する必要はないでしょう。
AsteriusModule
という独自のオブジェクトに変換します。ahc-ld
という専用のリンカーで、WASM向けにリンクします。ahc-dist
というコマンドで、リンクしたモジュールを実行できる状態にします。
+ahc-ld
がリンクしたモジュールを検証し、.wasm
ファイルに変換して、main
関数を実行する、エントリーモジュールを作ります。<script>
タグで参照すれば、ブラウザー上でHaskellが動きます。ここからは、私が以前作ったアプリケーションのコアに当たる関数をAsteriusでコンパイルすることで、ブラウザー上で動かせるようチャレンジした時の体験談を紹介します。
+今回試みたアプリケーションは、単純なコマンドラインアプリケーションです。
+詳細は省きますが、行単位で書かれたファイルをパースして、項目ごとの合計を計算するだけの、ありふれたものです。
+パーサーはmegaparsecを使って作り、整数の四則演算ができるようなっているのも特徴です。
+そのアプリケーションの処理のほとんどすべてに当たる、ファイル名とその中身を受け取って、計算結果を文字列で返す関数(FilePath -> Text -> Text
)を、FFIでエクスポート(foreign export javascript
)し、JavaScriptから呼べるようにしてみました。
アプリケーション自体の書き換えはほとんど必要なかったものの、依存関係を減らしたり、依存するパッケージを書き換えたりするのが大変でした。
+というのも、先ほど触れたとおり、Asteriusは現状「Template Haskellと、GHC標準におけるIOを行う関数(の大半)」が一切使用できないので、取り除かなければコンパイルエラーになってしまいます。
+template-haskellパッケージに間接的に依存しているだけで依存関係の解決すらできないのはなかなかつらいものでした。
+stack dot
コマンドを使って依存関係のツリーを作り、それを見てtemplate-haskellパッケージに間接的に依存しているパッケージを割り出し、そのパッケージの必要な関数のみを切り出すことでどうにか回避できました。
+monoidal-containersパッケージとfoldlパッケージがそれでした。
+幸い、どちらも依存しているのはごく一部だったで、必要な部分だけをコピペして使うことにしました。
+それから、IO
への依存もなくすために、textパッケージから*.IO
なモジュールを取り除いたりもしました。
当然、元々のアプリケーションもtextパッケージの*.IO
なモジュールを使ってはいたので、それを使わないよう修正する必要がありました。
+しかしそこはHaskell。そうしたIO
に依存した関数から純粋な関数を切り出すのは、型システムのおかげで大変楽ちんでした!😤
+入出力をするのにJavaScriptのFFIを使わないといけない、という現状のWebAssemblyの制約が、偶然にもマッチしたわけですね!
+純粋じゃない関数はときめかないので捨て去ってしまいましょう✨
ここまで頑張った結果、目的の関数をforeign export javascript
してコンパイルを通すことはできました🎉
+しかし、実際にブラウザー上で動かしてみたところ、AsteriusのFFIのバグにハマってしまいました…😢
+肝心のforeign export javascript
した関数が、返すべき値を返してくれないのです!
+恐らくforeign export javascript
を使わずに、Haskell側からJavaScriptの関数を呼ぶようにしていれば、今回の問題は回避できたのではないかと思います。
+しかし、それは今回のゴールではありませんし、あまり便利ではないのでひとまず移植は見送ることにしました。残念!
今回Asteriusを試したことで、ブラウザー上でHaskellを動かす、もう一つの可能性を知ることができました。
+とは言え、バグが多かったり依存関係からIOやTemplate Haskellを抜き出さなければならなかったりで、まだまだ実用的とは言い難いでしょう。
+しかし、今回報告したバグが直れば、ブラウザーによる処理のコアに当たる部分をHaskellで書く、という応用が利きそうです。
+例えばPandocなどHaskell製アプリケーションをブラウザーから操作する、なんてアプリケーション作りが捗りそうですね!
タイトルの通り、fallibleというパッケージを紹介します。
+ +ちなみに、fallibleはHaskell-jp Slackで:
+と質問したところ、該当するようなパッケージは無さそうだったので作ったという経緯があります。 +その際に助言をくれた fumieval氏のコードをほとんど引用した形になったので、Haskell-jp Blogに紹介記事を載せることにしました(僕は普段、自分のブログに自作したパッケージを書いています)。
+ +Haskellでアプリケーションを記述してると次のようなコードを書くことがありますよね?
+import qualified Data.List as L
+
+run :: String -> Token -> Bool -> IO ()
+= do
+ run targetName token verbose <- getUsers token
+ users case users of
+ Left err -> logDebug' err
+ Right us -> do
+ case userId <$> L.find isTarget us of
+ Nothing -> logDebug' emsg
+ Just tid -> do
+ <- getChannels token
+ channels case channels of
+ Left err -> logDebug' err
+ Right chs -> do
+ let chs' = filter (elem tid . channelMembers) chs
+ mapM_ (logDebug' . channelName) chs'
+ where
+ = logDebug verbose
+ logDebug' = "user not found: " ++ targetName
+ emsg = userName user == targetName
+ isTarget user
+logDebug :: Bool -> String -> IO ()
+= if verbose then putStrLn msg else pure () logDebug verbose msg
Slackのようなチャットツールをイメージしてください。
+該当の名前(targetName
)を持つユーザーを与えると、そのユーザーが参加しているチャンネルの一覧を表示するというような振る舞いです。
+こう段々になってしまうのは気持ち悪いですよね。
+fallibleの目的はこの段々を次のように平坦にすることです(where
などは割愛):
import Data.Fallible (evalContT, exit, lift, (!?=), (???))
+
+run :: String -> Token -> Bool -> IO ()
+= evalContT $ do
+ run targetName token verbose <- lift (getUsers token) !?= exit . logDebug'
+ users <- userId <$> L.find isTarget users ??? exit (logDebug' emsg)
+ targetId <- lift (getChannels token) !?= exit . logDebug'
+ channels $ mapM_ (logDebug' . channelName) $
+ lift filter (elem targetId . channelMembers) channels
というか、もともとのアイデアは下記のブログです:
+ +これを一般化(Maybe a
固有ではなく Either e a
でも使う)できないかなぁというのがもともとの発想です。
次の4つの演算子を利用します:
+(!?=) :: Monad m => m (Either e a) -> (e -> m a) -> m a
+(!??) :: Monad m => m (Maybe a) -> m a -> m a
+(??=) :: Applicative f => Either e a -> (e -> f a) -> f a
+(???) :: Applicative f => Maybe a -> f a -> f a
ただし、内部実装的には Maybe a
や Either e a
は Fallible
型クラスで一般化されています:
class Applicative f => Fallible f where
+type Failure f :: *
+ tryFallible :: f a -> Either (Failure f) a
+
+instance Fallible Maybe where
+type Failure Maybe = ()
+ = maybe (Left ()) Right
+ tryFallible
+instance Fallible (Either e) where
+type Failure (Either e) = e
+ = id
+ tryFallible
+(!?=) :: (Monad m, Fallible t) => m (t a) -> (Failure t -> m a) -> m a
+(???) :: (Applicative f, Fallible t) => t a -> f a -> f a
これらを継続モナドと組み合わせることでIOと失敗系モナド(Maybe a
や Either e a
)を、モナドトランスフォーマーなしにDo記法で書くことができます!
-- 継続モナドに関する関数
+evalConstT :: Monad m => ContT r m r -> m r
+
+exit :: m r -> ContT r m a
+= ContT . const exit
疑似的なIOで良いならfallibleリポジトリのexampleディレクトリにあります(上述の例はそれです)。
+実際の利用例であれば、最近自作したmatsubara0507/mixlogueというHaskellアプリケーションで多用しています(ココとかココとか)。
+ちなみに、mixlogueは特定のSlackの分報チャンネル(times_hoge
)の発言を収集するというだけのツールです。
READMEを参照してください。
+現状Hackageにはあげてないので、stackやCabalでGitHubリポジトリから参照する方法を利用してください。
+fumieval氏のコードをほとんど引用するだけになったので自分でリリースするか迷ったんですけど、リリースしてくれというのも丸投げがひどいので自分でリリースしました。 +まぁこういう結果が生まれるのもOSSコミュニティの醍醐味ということで。 +fumieval氏、いつもアドバイスをくれてありがとう!
+(もちろん他のHaskell-jpの皆さんも!)
+先日2019年11月9日、TECH PLAY SHIBUYAにてHaskell Day 2019を開催しました。
+今回は、各発表の概要や、アンケートの結果をお伝えしたいと思います。
まずは各種発表の紹介から。
+「関数型」 — すなわちHaskellでいうところのa -> b
で表される、Haskellの関数について、ちょっと見方を変えた新しい発見を教えてくれました。
Higher Kinded Datatype (HKD)という、昨今Haskell界で流行りの型定義方法を解説しています。
+レコード型を定義する際HKDにすることで、より柔軟に扱うことができるようになります。
+さらに、barbiesやextensibleといった、HKDの利用を飛躍的に促進するパッケージも紹介されました。
Haskellのderiving
機能 — 型を定義したとき、型クラスのインスタンスまで自動で定義してくれるあの機能ですね — の、適用範囲を広げるGHCの言語拡張をいろいろ紹介してくれました。
プログラミング言語Egisonの核となる機能である強力なパターンマッチを、GHCの各種拡張を駆使することで、Haskellのソースコードに自然に埋め込めるような形で実装した、という話です。
+ + +関数の自動微分を行うパッケージadの仕組みを自力で実装してみることで解説してくれました。
+ +misoというおいしそうな名前のアプリケーションフレームワークと、Firebaseと連携するmisoのサンプルを、ライブコーディングを通して紹介してくれました。
+misoを使えば、GHCJSを使ってElm Architecture風の設計に基づいてアプリケーションを作ったり、さらにそのコードを利用してサーバーサイドレンダリングをしたりできます。
ℹ️資料はまだ公開されていません!当日はライブコーディングが大半の時間を占めていたため、同等の解説を文章にして公開したいというチェシャ猫さんの意向によるものです。
+現在執筆中のためお待ちください。🙇
「mixi git challenge」というイベントにおいてユーザーが投稿した解答を採点するサーバーを、HaskellとElmで一から書き直した、という事例を発表してくれました。
+rioやservantといった著名なパッケージを使うだけでなく、足りないところを自力で補って新しいパッケージとして公開したり、さらに作成したアプリケーション自体をOSSとして公開したりすることで、大きな資産を残していただけました。
DeepFlow株式会社におけるHaskellの事例の紹介です。
+超高速で大規模な数値計算システムを、GHCの多様な言語拡張を駆使して作っているそうです。
+Tagless Finalを活用することで知っているべき領域を区分して仕事を分けることに成功しているという点が印象的でした。
cadenzaという、Truffle(GraalVMに含まれている、高速なインタープリター作成フレームワーク)製の関数型言語の紹介です。
+Truffleがもたらす強力なJITと「Normalization by Evaluation」という技術を応用することで、型検査と実行時両方における高いスピードを得ることが狙いだそうです。
+将来的には依存型言語における型チェックや、GHCのランタイムの高速化に寄与したいとのことです。
今回はHakell Day史上初めての試みとして、Lightning Talkを当日公募しました。
+残念ながら5分間という短い制限時間に収められない発表が大半でしたので、ぜひ👇の資料を読んでみてください!
順番が間違っていたら済みません!ご指摘を!
+Haskell Day の各セッションや、参加者のバックグランドなどについてアンケートをとりました。 +なので、後者の方の集計結果を載せたいと思います(前者はセッション発表者へのフィードバック)。
+大きく分けて Haskell に関する質問とそうでない質問がありました。 +まずは Haskell 以外に関する質問の方を集計結果を紹介します。
+ちなみに、言語のリストは Haskell Survey 2019 を参考にしました。
+自由記述形式にしたところ、ほとんど重複が無かったので箇条書きにします。
+ちなみに、Haskell に関する質問は Haskell Day 2018 でもアンケートしました。
+参照: https://wiki.haskell.jp/Links#書籍
+以上の発表に加えて今回は、下記のスポンサー企業の皆様や@fumievalくんのおかげで、大変満足度の高い懇親会ができました。
+ +発表について。
+昨年は「Haskellちょっと興味あるからちょっとできるまで」というテーマを意識して、発表の難易度別に時間帯が分かれるよう調整しましたが、残念ながらうまくいきませんでした。
+そこで難易度調整の難しさを痛感したため、今回は敢えて難易度調整を行わなかったのです。
+結果、全体として難しい発表に偏ってしまった点は少し反省です。私が会社で開いているHaskell勉強会に毎回参加いただいている同僚も、総じて難しくて追いつくのが大変だった、と仰ってました。
+次回は特別に難易度を下げた発表枠をいくつか作り、内容を事前に精査する、なんてプランを考えています。
会場について。
+TECHPLAY SHIBUYAは素晴らしいですね!
+我々のようなお金がないコミュニティーが今回の規模のイベントを行うのにうってつけでした。
+元々イベントを開催する前提で作られており、受付と演壇が近いため受付しながら発表を聞くことができるのも、持ち回りで受付をしている我々にとって好都合でした。
+来年も是非使わせていただきたいです🙏
最後に。
+発表者のみなさんはもちろん、支えていただいたスタッフ、スポンサー企業、会場まで足を運んでいただいた参加者の皆様、その他この会の実現に関わったすべての方々に、この場を借りて感謝の意を示したいと思います。
+みなさんのご協力おかげで、大きなトラブルもなく、楽しいイベントが開催できました。ありがとうございます。
+今後も日本Haskellユーザーグループ(a.k.a. Haskell-jp)をよろしくお願いします!
+hask(_ _)eller
はじめまして。趣味でHaskellしているはる(haru2036)と申します。まったり進行で開発しているのでGHCのバージョンアップの速さについていけてない感があります…… +さて、今回はあんまり深い話はありません。どちらかと言うとこんなニッチなところにHaskell使ったよというネタで書きます。
+突然ですが、私はVRChat(以下VRC)というソーシャルVRサービス(Second LifeのVR版みたいなものです)にハマっています。
+友人との雑談の中でVRCの中でLT会ができればプログラミングなどの話題で盛り上がれる人が集まってワイワイ楽しくできるのではないかと話して、その場のノリでとりあえず実装してみることにしました。
今回作りたかったのはスライドを表示するためのスクリーンと、ページ送りに使うボタンを実装したワールドです。
+VRCではアバターやワールドを自由に作ることができるのですが、VRCが提供するコンポーネント以外のスクリプトは利用できません。Haskellユーザとしては得意なことを活用しづらい土壌です。
+幸いスライドを表示する手段はゲーム内でURLから画像を取得し表示するVRC_Panoramaというコンポーネントを利用することにより確保できましたが、VRC_Panoramaが取得できる画像はワールド作成時に決め打ちで指定されたURLのリストに含まれるもののみです。
+そのため、スライド画像へのURLのリストを直接VRC_Panoramaに渡していると、イベントを開催する際よくある飛び入り参加やスライドの用意が遅れた参加者に対応できなくなってしまいます。
その問題を解決するために、イベント開始時からのページ数とスライドの画像URLをマップするWebAPIを用意しました。
+具体的には、/slides/{pageCount}
のような形のエンドポイントを持ったAPIを用意し、そこから実際の画像へリダイレクトをかけるという方法を取りました。
今回は自分で使うだけだしということでさらっとやってみたかったのでSpockを利用しました。もう少し誰でも使えるサービスにしたいと考えているのでServantに載せ替えてかっちり作り直そうかと思い移植しているところです。
+じつはLT会をやろうと思いついた友人のBOXPはClojureユーザで、せっかくだからとAPIの開発を手伝ってくれました。
+あまりHaskellに馴染みはなかったものの、いわゆる関数型プログラミング的な概念はバッチリなのでスススっと書いてくれました。
+書いてくれる上での障壁になったのは、型関連の要素(data
やtype
やnewtype
がぱっと見わからなかった、型コンストラクタ、値コンストラクタの概念)に馴染みが薄かったことでした。
+Discordで画面共有しながら説明を行ったのですが、やはり同じ画面を見ながら説明するのはとてもやりやすいと感じました。
+本人からのメッセージはこちら。
++プログラミングHaskellを昔読んでかじったことがある程度で素人もいいところでしたが、当人のサポートもあり思いついた数日後には実装が終わっていました。 +はるくんの話にもある通りDiscordで画面共有しながらペアプロし、Haskellでのテストコードの書き方も一から教えてもらいながら書きました。これは願ってもない体験だったので根気よく教えてくれたことに非常に感謝しています。
+また、個人的には実装以外でのブレストや実際の会場でのデバッグをVRChat上でできた事もとてもよかったなと思っています。 +単純に実装を確認するためには二人以上でVRChatに入る必要があるというのもありましたが、完全リモートでも身振り手振りありでブレストができたことや、アバターのおかげで環境に囚われないコミュニケーションができていたこともGoodでした。
+
今回は自分で使うだけな上に常時稼働している必要もなく、コストを最小限に抑えたかったのでHerokuにデプロイしました。
+Dockerfileを書き、スタティックリンク周りで悩みながらもイメージを生成してHerokuのレジストリにPushし、後はいつものHerokuという感じでうまくいきました。
余談ですが、最近参加したGotanda.hsというイベントでcabal build --enable-executable-static
でいい感じにシングルバイナリが生成できるというお話を聞いたので、最近stackばっかり使ってたのを改めて適材適所で使い分けていきたいなーと思っています。
ここはHaskellほぼ全く関係ないですが……
+
+VRC-LTという名前で6回ほど開催しているのですが、場所の制約を受けずに勉強会ができ、その後の懇親会も会場の撤収時刻や終電を気にせず話したい人はとことん話し続ける事ができるというところが非常に良かったです。
+ホワイトボードはまだ未実装ですが、空間に書けるペンも配布されているのでその手のアイテムも取り入れれば懇親会での話も更にはずむのではないでしょうか。
VRChatはPCのみでも利用することができます。 +VRC-LTはほぼ月イチペースで不定期開催ですので、もしよろしければ参加していただけると嬉しいです。 +開催時のアーカイブ等も以下のWebサイトにて公開中です。合わせてご覧ください。 +https://vrc-lt.github.io
+そんなこんなで開発中のリポジトリはこちらになります。
+拙いところもいっぱいですがIssueやPRなどで気になった点を教えていただければ幸いです!
+https://github.com/vrc-lt/VRC-Slide-Server
Haskell Symposium 2019にIIJとして参加してきました。
+聴講した発表についての概要をまとめましたので、どの論文を読んでみるか決めるなどの際にご活用ください。内容については私の聞きまちがい・読みまちがいなどあると思いますのでご了承ください。
+International Conference on Functional Programming(ICFP)に合わせて開催されるHaskellに関する国際会議です。Haskellに関する研究を発表したり、実践的な経験や将来の言語の開発について議論したり、その他の宣言的プログラミングを促進したりします。
+GADTと型クラスはそれぞれ便利だが混ぜると問題が起きる場合がある。
+次のようなTerm
があるとき、そのShow
インスタンスを考える。
data Term :: Type -> Type where
+Con :: a -> Term a
+ Tup :: Term b -> Term c -> Term (b, c)
次のようにShow
インスタンスを定義すると型エラーになる。
instance Show a => Show (Term a) where
+show (Con a) = show a
+ show (Tup x y) = unwords ["(", show x, ",", show y, ")"]
Could not deduce (Show b) arising from a use of `show'
+from the context (Show a) or from (a ~ (b, c))
+これはShow (b, c)
ならばShow b
という関係がないために起こる。
一方タプルについてのShow
は、Show a
かつShow b
ならばShow (a, b)
という関係である。
instance (Show a, Show b) => Show (a, b) where
+ …
この「ならば」を両方向にすれば問題は解決できるのではないかというのが、この論文の主張である。
+型クラスの法則は依存型を使えば証明できるが、インスタンスごとに書くのはめんどうなのでGenerics
で出来るようにしようという話である。
様々な種類の効果が複雑に絡み合うアプリケーションを整理するために、「効果を伴う処理を持った辞書」を明示的に渡す方式の提案である。
+提案した方式によってVeriFastを再実装してみることで、実際に発生した問題と解決方法を解説している。
+Coqによる、効果を伴うプログラムの証明に関する話。
+効果について直接証明することはせず、Freeモナドを用いての証明を試みても、そのままCoqに翻訳すると停止性チェックによってエラーになってしまう。
+そのために行った工夫に加え、具体例として、trace
や(部分関数による)エラーなど、Haskellにおいて暗黙に発生する効果を考慮したモデル化について検討した。
data kindsやtype familiesといったGHC拡張によって厳格なデータ型を定義できるが、それに対する操作を定義するとGHCには解けない型レベルの等式が生成されることがある。
+制約カインドの型に対する型クラスとしてProven
を提供し、この制約がある箇所をGHC型検査プラグインが検出して対応するCoqコードのテンプレートを生成する。
そのCoqコードに証明がなければ警告を表示する。
+type ProofName = Symbol
+
+class c => Proven (prf :: ProofName) (c :: Constraint)
+where {}
+
+applyProof :: forall prf c a. Proven prf c => (c => a) -> a
+= x
+ applyProof x
+= applyProof @"nonzero_pop" @(NNonZero (Popcount b) ~ True) Refl lemma3
いくつか制約があるがHaskellの型をCoqに自動的に変換している。
+REDFINという固定小数演算と整数演算のための処理系があるのだが、そのアセンブリーコードに対して形式検証をしたという報告である。
+G2QはHaskellのソースにquasi quoteで埋め込むDSLである。
+Haskellで書いた条件式をsymbolic executionして、SMT solverに渡す式に変換して、SMT solverに条件を満たす関数を導出させる。
+パフォーマンスのためにη変換してほしいところを明示したいことがある。
+例えば、次のような意味論上は等価な関数f1
とf2
があるとする。
= \x -> let z = h x x in \y -> e y z
+ f1 = \x -> \y -> let z = h x x in e y z f2
実際はf1
は引数x
を取った後クロージャー生成のためにヒープ確保するのに対して、f2
はアリティが2の関数と解釈されて中間のクロージャーが必要なくなる。
~>
というアリティの情報を持った関数型を新たに導入して->
の代わりに使えるようにする。
TYPE (a :: RuntimeRep (FunRep 2))
というような新たなポリモーフィズムを導入する。ここでの2
がアリティ。
Int
に対してInt#
があるように基本的にはパフォーマンスが必要なライブラリーなど内部的に使用する想定。
次のような準引用があったときに、組み合わせると元々あったはずの情報が欠落する場合がある。
+qshow :: Code (Int -> String)
+= [q| show |]
+ qshow
+qread :: Code (String -> Int)
+= [q| read |]
+ qread
+trim :: Code (String -> String)
+= [q| $(qshow) . $(qread) |] trim
qshow
とqread
にあったInt
という情報が、組み合わせてtrim
とすると欠落してコンパイルエラーになってしまう。
spliceするときにHaskellソースコードの構文木ではなくCoreに対するものを出力すればそれは型が明示されているし問題がない。
+しかも、splice後の型検査を省略できるのでコンパイルの高速化にも寄与する。
+souce pluginsのしくみや、書き方、実装時のテクニックの紹介である。
+同じ時刻のHIWの発表を聴講していたためこちらは聴講していません。
+同じ時刻のHIWの発表を聴講していたためこちらは聴講していません。
+同じ時刻のHIWの発表を聴講していたためこちらは聴講していません。
+モナドトランスフォーマーと代数的効果との対比である。
+モナドトランスフォーマーから代数的効果への変換またその逆のときにどういう手法があって、それぞれを構成する要素がどう対応しているかを説明している。
+モナドトランスフォーマーと代数的効果だとモナドトランスフォーマーの方が表現できるものが大きいのでモナドトランスフォーマーから代数的効果へはどんなものでも変換できるわけではない。
+例えばcatch
やlocal
は代数的効果にできない。
モナドはHaskell界隈で非常に普及しているのでSQLに対するEDSLとしてモナドの構造を採用したい。
+このときSQLの結合を表現すると、SQLとしてはスコープ外にもかかわらずEDSLとしてはスコープ内となって使える変数ができてしまう。
+これをEDSLとしてもエラーとしたい。
+例えば、次のような例で実行時エラーとなってしまう。ここでa0
はtableA
の列とする。
SELECT a0, b0
+FROM
+
+ tableALEFT JOIN
+ SELECT b0 FROM tableB WHERE a0 == b1)
+ (ON tableA.a2 == tableb.b2
SELECT b0 FROM tableB WHERE a0 == b1
の部分でスコープ外のa0
を参照しているためエラーとなる。
単純なモナドEDSLだと次のようになりコンパイルが通る。
+do
+:*: a1 :*: a2 <- from table0
+ a0 $ do
+ leftJoin :*: b1 :*: b2 <- from table1
+ b0 $ a0 .== b1
+ ristrict $ a2 .== b2 on
ristrict $ a0 .== b1
の部分においてa0
はHaskellとしてはスコープ内にある。
この問題を次のような型レベル関数を駆使することでEDSLにおいてもコンパイル時エラーとすることができた。
+type family Cols a
+type family Outer a
+type family UnAggr a
+type family FromRow a
前回から引き続き、Haskell Implementors’ Workshop 2019への参加レポートとして、私の印象に残った発表を紹介します。
+今回は、Copilotという、C言語のコードを生成するHaskell製内部DSLについての発表です。
発表者: Frank Dedden Royal Netherlands Aerospace Center, Alwyn Goodloe NASA Langley Research Center, Ivan Perez NIA / NASA Formal Methods
+Haskell製の内部DSLからC言語のソースコードを生成する、Copilotの紹介です。
+似た謳い文句の内部DSLとしてivoryがありますが、Copilotは、ハードウェアの実行時検証を行うC言語のコードを生成することに、より特化しています。
+「センサーから信号を受け取って、一定の条件を満たした場合に何らかの処理を実行する」という処理をHaskellで宣言的に記述すると、メモリの消費量・実行時間において常に一定なC言語のコードを生成することが出来ます。
メモリが限られていて、リアルタイムな処理が必要なハードウェアにとって「邪魔にならない監視」を実現するための必須条件なのでしょう。
+現状HaskellはGCが必要であるといった制約もあり、リアルタイムな処理や厳格なメモリー管理が必要な機器での採用は難しいですが、Ivoryや今回発表されたCopilotはあくまでも「C言語のコードを生成するだけ」なので、生成するHaskellではメモリー管理をする必要がありません。
+にっくきスペースリークに悩まされる心配もないのです。
+こういったHaskell製内部DSLは、Haskellの持つ強い型付けによるメリットを享受しながら、変換した言語の実行時におけるパフォーマンスを出しやすい、といういいとこ取りなメリットがあるので、もっと広まってほしいユースケースですね。
せっかくなんでCopilotを試してみましょう。
+公式サイトにあったサンプルコードそのまんまですが、生成されるCのコードを眺めてみます。
👇のコマンドでサンプルコードが入ったリポジトリーをgit cloneした後、
+git clone https://github.com/haskell-jp/blog
+cd blog/examples/2019/hiw-copilot
👇のコマンドでビルド・C言語によるコードの生成できるはずです。
+stack build copilot
+stack exec runghc heater.hs
こちらが生成元のHaskellのコードです。
+import Language.Copilot
+import Copilot.Compile.C99
+
+import Prelude hiding ((>), (<), div)
+
+temp :: Stream Word8
+= extern "temperature" Nothing
+ temp
+ctemp :: Stream Float
+= (unsafeCast temp) * (150.0 / 255.0) - 50.0
+ ctemp
+= do
+ spec "heaton" (ctemp < 18.0) [arg ctemp]
+ trigger "heatoff" (ctemp > 21.0) [arg ctemp]
+ trigger
+= reify spec >>= compile "heater" main
まず、temp
とctemp
という識別子に定義した式が、センサーが発信する、連続的に変化する値を表しています。
+Copilotの言葉はこれをStream
と呼んでいます。
spec
という識別子で定義している式が、「どのセンサーから信号を受け取って、どんな条件を満たした場合にどの処理を実行するか」規定しているようです。
+👆の場合、ctemp
というStream
が18.0
を下回ったらheaton
というイベントを発火し、21.0
を超えたらheatoff
というイベントを発火する、と定めているわけですね。
+そしてmain
関数で実行しているreify spec >>= compile "heater"
という箇所で、.h
ファイルと.c
ファイルを書き込んでいます。
そして、生成されたヘッダーファイルheater.h
がこう👇
extern uint8_t temperature;
+void heatoff(float heatoff_arg0);
+void heaton(float heaton_arg0);
+void step(void);
で、肝心のCのコード本体heater.c
がこちらです。
#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include "heater.h"
+
+static uint8_t temperature_cpy;
+
+bool heatoff_guard(void) {
+return ((((float)(temperature_cpy)) * ((150.0) / (255.0))) - (50.0)) > (21.0);
+ }
+
+float heatoff_arg0(void) {
+return (((float)(temperature_cpy)) * ((150.0) / (255.0))) - (50.0);
+ }
+
+bool heaton_guard(void) {
+return ((((float)(temperature_cpy)) * ((150.0) / (255.0))) - (50.0)) < (18.0);
+ }
+
+float heaton_arg0(void) {
+return (((float)(temperature_cpy)) * ((150.0) / (255.0))) - (50.0);
+ }
+
+void step(void) {
+(temperature_cpy) = (temperature);
+ if ((heatoff_guard)()) {
+ (heatoff)(((heatoff_arg0)()));
+ };
+ if ((heaton_guard)()) {
+ (heaton)(((heaton_arg0)()));
+ };
+ }
先ほどStream
として定義した値のうち、temp
は、temperature
というグローバル変数と、それを一時的に保存するtemperature_cpy
という二つの変数に翻訳されました。
+spec
においてtrigger
という関数で列挙した「どのセンサーから信号を受け取って、どんな条件を満たした場合にどの処理を実行するか」というルールは、step
という関数に現れたようです。
+この関数を利用する側では、heaton
関数とheatoff
関数を別途定義した上で、temperature
にセンサーから受け取った値を代入してstep
を呼ぶことによって、temperature
の値が条件に一致したとき、heaton
関数とheatoff
関数を実行してハードウェアの制御ができるのでしょう。
+Haskell側で定義したもう一つのStream
、ctemp
は、heaton_guard
、heaton_arg0
、heatoff_guard
、heatoff_arg0
、それぞれの関数に書かれた、temperature_cpy
の値を変換する式に現れているようです。
正直なところこの程度であれば、直接Cで書いた方が余計なカッコもないし読みやすそうではあります。
+temp
をctemp
に変換する式(150.0 / 255.0) - 50.0
が変換後のソースコードでは冗長に適用されていることから、もっと最適化できそうですし。
+とはいえ、わざわざDSLを作ったからには、より複雑で、Haskellでなければ書いてられないようなケースが、Copilotの開発者の現場ではあるのでしょう(なんせNASAの方も関わっているぐらいですから!)。
+詳しいユースケースや、ビルド時のフローといった運用方法を聞きたいところですね。
前回から引き続き、Haskell Implementors’ Workshop 2019への参加レポートとして、私の印象に残った発表をいくつか紹介します。
+今回は、「GHC 8.10に導入されるであろう機能」です。
+いずれも該当するMerge Requestはmasterブランチにマージ済みなので、おそらくGHC 8.10で提供されるでしょう。
昨年のHaskell Symposiumでも発表されてGHC 8.6で導入された、「Valid Hole Fits」という機能のさらなる拡張について。
+まず、「Valid Hole Fits」という機能について軽く紹介します(詳しくはこちらのスライドが参考になるかと思います)。
+「Valid Hole Fits」はアンダースコア _
で始まる識別子を書いたとき、GHCが推論した型にマッチする関数をエラーメッセージに付記することで、ユーザーがどんな式を書けばよいか、ヒントを与えてくれるものです。
例えば、
+map (length . _someFunc) [True, False, True]
上記のように、アンダースコア _
で始まる識別子を書いたとき、
<interactive>:1:16: error:
+...
+ Valid hole fits include
+ enumFrom :: forall a. Enum a => a -> [a]
+enumFrom @Bool
+ with Prelude’ (and originally defined in ‘GHC.Enum’))
+ (imported from ‘ show :: forall a. Show a => a -> String
+show @Bool
+ with Prelude’ (and originally defined in ‘GHC.Show’))
+ (imported from ‘ repeat :: forall a. a -> [a]
+repeat @Bool
+ with Prelude’ (and originally defined in ‘GHC.List’))
+ (imported from ‘ return :: forall (m :: * -> *) a. Monad m => a -> m a
+return @[] @Bool
+ with Prelude’ (and originally defined in ‘GHC.Base’))
+ (imported from ‘ pure :: forall (f :: * -> *) a. Applicative f => a -> f a
+pure @[] @Bool
+ with Prelude’ (and originally defined in ‘GHC.Base’))
+ (imported from ‘ mempty :: forall a. Monoid a => a
+mempty @(Bool -> [a0])
+ with Prelude’ (and originally defined in ‘GHC.Base’)) (imported from ‘
といった具合に、アンダースコアで始まる識別子_someFunc
の型をBool -> [a0]
と推論した上で1、実際にその型に該当する関数を、当該のスコープにおいてアクセスできる関数の中から探して教えてくれる、それが「Valid Hole Fits」という機能です。
今回発表された「HoleFitPlugins」という機能は、名前のとおりこの「Valid Hole Fits」に対するプラグイン機構です。
+「Valid Hole Fits」が表示する「型にマッチした関数」を探す処理を、Haskellのコードで書き換えられるようにしてくれます!
「そこまでする必要あるの?」という気もしてきますが、発表者曰く
+という意図があるそうです。
+最新安定版のGHCでは利用できませんが、ドキュメントがこちらにあるので、GHCのHEAD(masterブランチで開発中のバージョン)をコンパイルすれば使用できるようです。
+加えて発表では、_
で始まる識別子を書く際の構文を拡張することで、どのようにcandidateを探すか指定できるようにする、なんて機能も紹介されました(ドキュメントを読む限りこの機能はまだHEADに入ってない?)。
+例えば、Hoogleを使ってValid Hole Fitsを探したいとき、次のように書くことで検索対象をControl.Applicative
に限定する、といったことをできるようしてくれます。
g :: [a] -> [[a]]
+= _{hoogleLookup "+Control.Applicative"} g
Valid Hole Fitsの検索方法をその場で微調整したい、というときに使うものですね。
+タイトルのとおり、「Visible dependent quantification」という機能の紹介です。
+最近のバージョンのGHCiにおける:kind
コマンドは、次のような、GHCがサポートしていない構文の型注釈を出力することがあります。
+例えば
> :set -XKindSignatures
+> :set -XPolyKinds
+> data SomeType k (a :: k)
+> :kind SomeType
+SomeType :: forall k -> k -> *
における、SomeType :: forall k -> k -> *
のforall k ->
という部分です。
+現在のHaskellでforall k
などと書くときは、必ず
SomeType :: forall k. k -> *
といった具合に、ピリオドで区切った構文になります。
+ところが先ほどの:kind
の出力では、forall k ->
とあるとおり、forall k
に(型ではなく、カインドとしての)関数を表す->
が使われています。
+「Visible dependent quantification」はまさにこれを、:kind
コマンドによって出力される構文だけではなく、ユーザーが直接書ける構文にしよう、というものです。
+GHCに「依存型」という機能を加える「Dependent Haskell」にも必要な機能だそうです。
+私自身はこの機能を使う機会がちょっと思い浮かばなかったので省略しますが、より詳しい解説は発表者であるRyan自身による記事(英語)をご覧ください。何が「Visible」でどう「Dependent」なのかわかるはずです。
復習: この、「アンダースコアで始まる識別子_someFunc
の型をBool -> [a0]
と推論した上で」エラーメッセージにおいてFound hole: _someFunc :: Bool -> [a0]
と教えてくれるのが「Type Hole」という機能なのでした。↩︎
こんにちは。
+今回からいくつか、「Haskell Implementors’ Workshop 2019」に私が先月参加した際のレポートとして、印象深い発表をテーマごとに分けた短い記事を執筆します。
+最近公開されたGHC 8.8の話はもちろん、未来のGHCやその他のHaskellの処理系を知るのによいイベントでしたので、その一部だけでも伝われば幸いです。
シリーズ(?)第1回目なので、簡単にHIWそのものについて紹介しておきましょう。
+HIWは、ICFP (International Conference on Functional Programming)という関数型プログラミングについての国際会議に併設された、Haskellの実装者のためのワークショップです。
+名前の通り、GHCをはじめとするHaskellの処理系(あるいは、Haskellで実装された言語処理系)の実装に関する発表だけでなく、かなり緩いテーマのLightning Talkの時間があったり、GHCの将来の方向性について自由に議論する時間もあったりしました。
今回はそのうち、掲題のとおり「HIW 2019で発表された、GHC 8.8で導入された機能」を紹介します。まずは「HIE files in GHC 8.8」から。
+発表者: Zubin Duggal, Matthew Pickering University of Bristol
+GHC 8.8で新たに追加された、HIE(「Haskell Interface Extended」の略と思われます)ファイルについての発表です。
+コンパイル時にGHCが得たモジュールの情報を、Haskell IDE EngineなどのIDEのバックエンドが再利用しやすい形で出力する機能です。
+従来Haskell IDE Engine(その裏で使われているghc-mod)やghcid、interoなどの、「IDEバックエンド」(エディターが入力の補完や入力したソースコードにおけるエラーを表示する際に通信するソフトウェア)は、自前でGHC APIやGHCiを呼ぶことで、型チェックしたり定義ジャンプに必要な位置情報を収集したりしていたのですが、そうした情報の収集をすべてGHC自身がHIEファイルを出力することで賄えるようになる、ということです。
私は従来開発中、stack test --pedantic --file-watch
などとNeovimのターミナル機能で実行して実行ファイルをビルドしつつ、HIEにエラーの表示や入力の補完をさせていたのですが、その際も二重にソースコードが解析されていたんですね!
+私がそのようにわざわざstack test
とHIEを並行して実行させているのは、HIEがしばしばフリーズしてしまったり(Neovimごと再起動すれば直ることも多いんですが…😰)、HIEだけでは実行ファイルの作成やテストの実行ができない、という理由があるためです。
+stack test
だけでHIEファイルが生成されるようになれば、エラーに関する情報やソースコードの解析結果といった情報が一元化されるので、より安定的に、より少ないリソースでHIEが使えるようになるでしょう。本家Haskell IDE Engineがサポートする日が楽しみです。
この、HIEファイルを利用するアプリケーションの例も紹介されました。
+hie-lspという小さなLanguage Server Protocolの実装に加え、hie-lsifという、HIEファイルから「Language Server Index Format (LSIF)」形式のファイルを作成するコマンドが印象的でした。
+このLSIFというファイルは、例えばGitHubのリポジトリ上でブラウザからソースコードを閲覧する際にも、定義ジャンプといった便利な機能を使えるようにするためのものです。リポジトリに置いたソースコードを処理系がどのように解釈したかを保存しておくことで、Language Serverはじめ処理系を実行することなく利用できるようにするものだそうです。
+現状は仕様策定中なためか、実際にLSIFを解釈するアプリケーションは見つかりませんでしたが、今後の活用に期待が高まりますね。
加えて、HIEファイルが将来的にサポートしたい機能などについても発表されました。
+.hi
という拡張子で出力されているあのファイル)」との統合発表者: Simon Peyton Jones Microsoft, UK
+GitLabへの移行やHadrianと呼ばれる新しいビルドシステムの導入など、インフラ周りでいろいろ変更があったこともあり、遅れてしまいましたがGHC 8.8がもうすぐ出るよ、という内容の発表でした(発表当時。もうGHC 8.8はリリースされています)。
+言及された主な追加機能は以下のとおりです。
+TypeApplications
という言語拡張が、型変数だけでなくカインド変数に対しても適用できるようになりました。
+正直に言って、個人的に使いどころがまだまだなさそうな機能ではありますが…。
ScopedTypeVariables
という言語拡張を使った場合に、パターンマッチした変数に型注釈を付けることができるようになる、という機能がありまして、これが拡張されました。
+具体的には、従来下記のように書くことで、関数自体の型注釈にある型変数a
と、パターンマッチした変数x
に型注釈したb
が等しくなるように書くことができたのを、
f :: forall a. Maybe a -> Int
+Just (x :: b)) = {- ... -} f (
さらに拡張して、関数自体の型注釈にある型変数ではないInt
と、パターンマッチした変数x
に型注釈したb
が等しくなるように書くことができるようにした、という拡張です。
+あたかも型変数でパターンマッチしているかのようですね。
f :: Maybe Int -> Int
+Just (x :: b)) = {- ... -} f (
一体何の役に立つの?とも思いましたが、この修正に向けた提案曰く、
+f :: ReallyReallyReallyReallyLongTypeName -> T
+x :: a) = … (read "" :: a) … f (
と書くことで、長い型名に対して別名を付けることができるようになる、というメリットがあるそうです。なるほど💡
テーマを絞って短い記事にした方がSEO的にいいんじゃないかと思いまして、今回は敢えて紹介する発表を絞りました。
+今後は下記のテーマについて紹介する予定です。
また、HIWと同じくICFP 2019に併設して開催された、Haskell Symposium 2019の発表についても別途共有する予定です。
+乞うご期待。
+hask(_ _)eller
前回から引き続き、Haskell Implementors’ Workshop 2019への参加レポートとして、私の印象に残った発表を紹介します。
+今回は、Gibbonという、GHC以外のHaskell(の、サブセット)の処理系についての発表です。
発表者: Ryan R. Newton Indiana University, Michael Vollmer Indiana University, USA, Chaitanya Koparkar Indiana University
+Gibbonは最適化の手法を研究するために作られたコンパイラーです。
+具体的には、我々(特にHaskeller)がよく使う、木構造全体に対する処理の最適化です。
こうした木構造のデータは、通常ポインターを使ってメモリー内にバラバラに格納されますが、Gibbonによる最適化を行うと、実際にプログラムがどのような順番で木を処理しているのか解析して、(元のデータ構造を配列に変換した上で)その順番に並べられた配列として処理するコードに変換する、という大胆な変換を行います。 +図にするとこんなイメージでしょうか?
+👆のような木構造があったとして、
+👆における、赤い線の順番(行きがけ順)にアクセスする関数があったとします。
+適当にHaskellの再帰関数として書くと、👇こういうコードです。
data Tree = Node Char (Maybe Tree) (Maybe Tree) deriving Show
+
+tree :: Tree
+=
+ tree Node 'A'
+ Just
+ ( Node 'B'
+ ( Just (Node 'D' Nothing Nothing))
+ (Just (Node 'E' Nothing Nothing))
+ (
+ )
+ )Just
+ ( Node 'C'
+ ( Just (Node 'F' Nothing Nothing))
+ (Just (Node 'G' Nothing Nothing))
+ (
+ )
+ )
+preOrder :: (Char -> IO ()) -> Tree -> IO ()
+Node char mLeft mRight) = do
+ preOrder access (
+ access char
+case mLeft of
+ Just left -> preOrder access left
+ Nothing -> return ()
+
+case mRight of
+ Just right -> preOrder access right
+ Nothing -> return ()
Gibbonはこの関数と、それが処理する木構造を解析して、
+👆のような、ただの配列(とそれに対する関数)にまとめて変換してしまう、というのです!
+現代のコンピューターは、このような配列の要素にまとめてアクセス処理する方が、ポインターをたどって各要素を処理するより、たいてい遙かに速いです。
+Gibbonはこの特性を活かすべく、我々Haskellerが好んで使うような、ポインターだらけの木構造を可能な限り配列に変換することで、要素をまとめて処理する(traverseする)演算の最適化を図るコンパイラーです。
ちなみに、元の木に対するノードの追加に相当する処理は、新しいノードに対するポインターを書き込む処理に変換するそうです。
+なので何度も追加を繰り返すと、あまり恩恵が受けられなくなってしまいそうです。
なかなか興味深いアイディアですが、個人的に聞きそびれた疑問が2つあります。
+一つは、そもそも木構造を定義するような状況というのは、いろいろな順番でアクセスしたいし、新しい要素の追加も繰り返し行いたいケースではないでしょうか?
+例えばunordered-containersにあるHashMap
型は探索木を使った頻繁に使われるデータ構造ですが、HashMap
を使う場合に行う処理の多くは、ランダムアクセスや要素の追加・削除でしょう。
なので、Gibbonが最適化したい「木構造」というのは、どちらかというと探索木のような木ではなく、構文木のような、要素をまとめて処理することを前提とした木のことなのかもしれません。
+確かに人間が書く言語の構文木程度であれば、すべてメモリー上で処理できる程度のサイズに収まる(という想定でなければコンパイラー作りがものすごく難しくなる)でしょうし、構文木の処理を高速化できれば、遅い遅いと言われるGHCのコンパイル速度も高められるはずです。それはそれでありがたい。
もう一つは、これまた例えばHashMap
型のような木をベースにした連想配列も、配列ベースのハッシュテーブルに変換することができるのでしょうか?
+もしそうだとすると、ランダムアクセスに対する計算量のオーダーもO(log n)からO(1)に変わるわけですし、要素をまとめて処理する以外の演算についても劇的な改善が見込めるかもしれません。
+もちろんこれも先ほどの推測が正しければ無意味な想像ですが、夢のある話ですね。
Gibbonは将来的には、Packed
という型クラスを提供することで、GHC本体への統合も視野に入れているそうです。
+Packed
を実装した型は、値をどのように配列に変換するのか定義することで、Gibbonによる最適化のためのヒントを与えることができます。
Haskell-jpのコンテンツの一つとしてHaskell Antennaという Web ページの開発・運用をしております。
+このWebページはHaskell-jpのリポジトリで開発し、GitHub Pagesとして公開しています。 +更新はTravisCIのDaily Cronを使って行なっていましたが、なんとかして 毎時更新を実現したい と思案していました。 +ひょんなことからDrone CloudというCIサービスを見つけ、このサービスではHourly Cronが可能だったので試しに更新の部分だけ移行してみました。 +という話です。
+ちなみに、本稿は全くHaskellのコードが出て来ません ;)
+Drone Cloudはパブリックリポジトリであれば無料で利用できるCIサービスです。 +内部ではDroneというOSSのCIプラットフォームを利用しています。 +Droneは以下のような特徴を持っています:
+OSSのためお好みのクラウドサービスなどで自前運用も可能ですし、Drone Enterpriseという有料のクラウドサービスも提供しています。
+単純にDroneの設定ファイルを記述するだけではなく、次のような作業を行いました:
+順に説明していきましょう。
+Drone Cloudでは無償でキャッシュを利用することができません1。
+Haskell(というかStack)のCIをしている方ならわかると思いますが、キャッシュなしにビルドするとすごい時間がかかります。
+なので、Haskell Antennaの静的ページの生成を行う antenna
コマンドをDockerイメージとしておき、Drone内ではこのイメージを利用して静的ページの生成を行う方針でいきます。
もちろんDockerイメージの生成には、StackのDockerインテグレーションを使います。 +以下のような設定をstack.yamlに追記し:
+# stack.yaml
+docker:
+ repo: fpco/stack-build
+ enable: false
+image:
+ container:
+ name: antenna
+ base: fpco/ubuntu-with-libgmp
次のようなコマンドを実行するだけでantenna
というDockerイメージが生成されます:
stack docker pull
+stack --docker image container
+Docker Hubにhaskelljp/antenna
というネームスペースを確保した2のでここにプッシュしました:
docker tag antenna haskelljp/antenna
+docker push haskelljp/antenna
+ちなみに、haskell-jp
ではなく haskelljp
なのは、Docker Hubの組織アカウント名には -
が使えなかったからです。
Docker HubにはAutomated buildsと呼ばれるGitHubなどのプッシュによって行う自動ビルドがありますが、StackのDocker Integrationを使うと、その機能を利用することができません。 +そこで、TravisCIを使って自動ビルドすることにしました。 +この辺りは「Stack の Docker Integration とイメージの自動更新 - ひげメモ」で記事にしてあるので、細かい話は割愛します。
+今回は次のような設定をして、master ブランチにプッシュがあった時にのみDocker Hubにプッシュします:
+# .travis.yml
+
+# ...
+
+install:
+- mkdir -p ~/.local/bin
+- export PATH=$HOME/.local/bin:$PATH
+- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
+- stack docker pull
+jobs:
+ include:
+ - stage: build dependencies
+ script: stack --no-terminal --docker --install-ghc test --bench --only-dependencies
+ - stage: build antenna
+ script: stack --no-terminal --docker build --bench --no-run-benchmarks --no-haddock-deps --pedantic
+ - stage: push docker image
+ if: branch = master AND type = push
+ script:
+ - stack --docker image container
+ - docker tag antenna haskelljp/antenna
+ - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
+ - docker push haskelljp/antenna
DockerでHaskellのビルドもするために毎回Docker Pullが走るようになり少し遅くなったのが辛いですね(今後要検討)。
+いよいよDroneによる antenna
コマンドの実行を設定します。
+元々は TravisCI でこんな感じでした:
jobs:
+ include:
+ - stage: install anttena
+ script: stack --no-terminal install
+ - stage: exec antenna
+ script: git clone -b gh-pages "https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git" temp
+ if: branch = master AND type IN (push, cron)
+ after_success:
+ - cp sites.yaml temp/sites.yaml
+ - cp -r image/* temp/image
+ - cd temp
+ - stack exec -- antenna sites.yaml
+ - git config user.name "${GIT_NAME}"
+ - git status
+ - git add -A
+ - git diff --quiet && git diff --staged --quiet || git commit -am "[skip ci] Update planet haskell. See https://haskell.jp/antenna/ for new entries!"
+ - git push origin gh-pages
stack install
で antenna
コマンドの実行ファイルを生成し(これはキャッシュされるので早い)、gh-pages
ブランチへページの更新をプッシュするためにGitHubのPersonal Tokenを使って再度クローンしていました。
+更新の有無は git diff
を使って確認しています。
まずはこれをこのままDroneに移植します:
+# .drone.yml
+
+kind: pipeline
+name: default
+
+steps:
+- name: exec antenna
+ image: matsubara0507/antenna
+ environment:
+ GH_TOKEN:
+ from_secret: github_api_token
+ GIT_NAME: BOT
+ commands:
+ - git clone -b gh-pages "https://${GH_TOKEN}@github.com/haskell-jp/antenna.git" temp
+ - cp sites.yaml temp/sites.yaml
+ - cp -r image/* temp/image
+ - cd temp
+ - antenna sites.yaml
+ - git config user.name "${GIT_NAME}"
+ - git status
+ - git add -A
+ - git diff --quiet && git diff --staged --quiet || git commit -am "[skip ci] Update planet haskell. See https://haskell.jp/antenna/ for new entries!"
+ - git push origin gh-pages
+ when:
+ branch:
+ - master
+ event:
+ exclude:
+ - pull_request
記法は違うもののそのまま移植ができました。
+条件(if
やwhen
)のところですが、Droneでcron
のイベントを指定する方法がわからなかったので、動作しては困るpull_request
だけ弾くようにしました。
Cronの設定はWeb UI上で行います:
+これで毎時間master
ブランチのビルドが実行されます。
Personal Tokenは他の個人のリポジトリも操作できてしまうので、兼ねてからできれば使いたくないなと思っていました(特に個人プロジェクトじゃないHaskell-jpのプロジェクトでは)。 +なので、これを機にリポジトリ固有のDeploy Keyに移行しました。
+CircleCIのような書き込み用のSSH Keyを登録する機能はDroneにはありません。 +代わりに次のように書くと良いです:
+steps:
+- name: clone gh-pages
+ image: docker:git
+ environment:
+ SSH_KEY:
+ from_secret: deploy_key
+ commands:
+ - mkdir /root/.ssh && echo "$SSH_KEY" > /root/.ssh/id_rsa && chmod 0600 /root/.ssh/id_rsa
+ - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > /root/.ssh/config
+ - git clone -b gh-pages git@github.com:haskell-jp/antenna.git temp
SecretというのはDrone側で保持・秘匿できる環境変数のような機能です(名前が違うだけでだいたいどのCIプラットフォームにもありますね)。
+今回はせっかくなので、これをDrone Pluginとして自作した3、matsubara0507/git-with-ssh
というのを使います:
# .drone.yml
+kind: pipeline
+name: default
+
+steps:
+- name: clone gh-pages
+ image: matsubara0507/git-with-ssh
+ settings:
+ ssh_private_key:
+ from_secret: deploy_key
+ ssh_hosts:
+ - github.com
+ commands:
+ - git clone -b gh-pages git@github.com:haskell-jp/antenna.git temp
+ when:
+ branch:
+ - master
+ event:
+ exclude:
+ - pull_request
+
+- name: exec antenna
+ image: haskelljp/antenna
+ commands:
+ - mkdir -p temp
+ - cp sites.yaml temp/sites.yaml
+ - cp -r image/* temp/image
+ - cd temp
+ - antenna sites.yaml
+
+- name: push gh-pages
+ image: matsubara0507/git-with-ssh
+ settings:
+ ssh_private_key:
+ from_secret: deploy_key
+ ssh_hosts:
+ - github.com
+ commands:
+ - cd temp && git config user.name BOT
+ - cd temp && git config user.email bot@example.com
+ - cd temp && git status
+ - cd temp && git add -A
+ - cd temp && git diff --quiet && git diff --staged --quiet || git commit -am "[skip ci] Update planet haskell. See https://haskell.jp/antenna/ for new entries!"
+ - cd temp && git push origin gh-pages
+ when:
+ branch:
+ - master
+ event:
+ exclude:
+ - pull_request
cd temp &&
というのがダサいですが、そこはおいおい直します(git-with-ssh
の方を)。
元々はHaskell-jpの #antenna
チャンネルにGitHubの通知設定をしているだけでした。
+今回の開発中、ずっとコミットの通知などが来てうるさかったので次のように分けました:
#antenna
チャンネルは gh-pages
ブランチのコミットだけ通知
+#antenna-dev
チャンネルを新しく作りGitHubの全ての更新はこっちに設定#dockerhub
チャンネルもついでに作って haskelljp
の更新を通知する最近、Docker Hubの大リニューアルがあって、いつのまにかDocker HubとSlackを連携できるようになっていました。 +なので試しに連携して更新の通知が飛ぶようにしてみました:
+いくつかあります:
+あと、QiitaのFeedがコメントや追記などでも更新され、その通知が #antenna
チャンネルに飛んで来てうるさいので修正したいです4。
キャッシュの導入の仕方は記事にしたので興味のある方は是非「GCS で Drone 1.0 をキャッシュする - ひげメモ」↩︎
もしDocker Hubのhaskelljp組織アカウントのメンバーになりたい場合はHaskell-jp Slackで声をかけてください(チャンネルはどこでも良いですよ)。↩︎
この話も記事にしておきました「Drone Plugin を作ってみた: git-with-ssh - ひげメモ」↩︎
この修正は haskell-jp/antenna
ではなく、matsubara0507/scrapbook
からやる必要があります。↩︎
先日、といっても2019年10月18日のことなんでもう2ヶ月以上も経ってしまいましたが、私はRegex Festaというイベントで、「regex-applicative」というパッケージの紹介を致しました。
+今回はその際使用したスライドを、ブログ記事として詳しく共有させていただきたいと思います!
+発表時のスライドと比べて、よりHaskellを知っている人向けになってしまいますが、regex-applicativeの魅力を明確に伝えるために必要なのでご了承ください。
+Applicativeスタイルを前提知識とします。
sym :: Eq s => s -> RE s s
pure :: a -> RE s a
(*>) :: RE s a -> RE s b -> RE s b
・string :: Eq a => [a] -> RE a [a]
(<|>) :: RE s a -> RE s a -> RE s a
many :: RE s a -> RE s [a]
・some :: RE s a -> RE s [a]
optional :: RE s a -> RE s (Maybe a)
regex-applicativeは、正規表現をHaskellの内部DSLとして表現したライブラリーです。
+名前のとおり、いわゆる「Applicative
スタイル」で正規表現を書くことができます。
regex-applicativeには、正規表現オブジェクトRE
型の値とマッチさせる文字列を受け取って、その結果を返す関数がいくつかあります。
+今回はそのうち最も単純なmatch
関数を使用します。👇のような型定義となっています。
match :: RE s a -> [s] -> Maybe a
定義のとおり、RE
型は型引数としてマッチさせる文字の型s
と、マッチした結果にも使われる「正規表現の結果」を表す型a
を受け取ります。
+RE
型をApplicative
のインスタンスにするためには、その結果を表す型が必須なのです。この後出す例でこの「正規表現の結果」を好きな値に変える方法を示しましょう。
そして第2引数がマッチさせる文字列に当たります。[s]
とRE
型の第1型引数s
のリストになっているとおり、match
関数(と、その他のregex-applicativeにおいて文字列をマッチさせるAPI)は任意のリストに対して使用することができます。
+Haskellの標準の文字列String
の実態は[Char]
、すなわちChar
のリストなので、通常regex-applicativeを使用する場合s
にはChar
が割り当てられます。
+型変数なので、当然他の型のリストに対しても使用できます。これは他の正規表現ライブラリーではあまりない特性でしょう。
戻り値はおなじみのMaybe
型です。マッチが成功すれば、引数に渡した正規表現RE s a
型の「結果」、a
型の値をJust
にくるんで返します。そして失敗すればもちろんNothing
を返します。
⚠️match
関数について特筆すべきことをもう一つ。他のよくある正規表現ライブラリーと異なり、match
関数は完全一致じゃないとマッチしないのでご注意ください。
+regex-applicativeには完全一致じゃないといけない関数と完全一致じゃなくてもよい関数両方があるので、少し混乱します😰
それではいよいよregex-applicativeパッケージを使ってみましょう。
+👇のコマンドでインストールして、GHCiで試します。
stack build regex-applicative
+stack exec ghci
(最近の)cabal
の場合は👇を実行すればできるはずです。
cabal v2-install --lib regex-applicative
+cabal v2-repl -b regex-applicative
GHCiが起動したら、こちらのimport
文を張って、本記事のサンプルを実行する準備をしてください。
import Text.Regex.Applicative
+import Text.Regex.Applicative.Common
sym :: Eq s => s -> RE s s
ここからは、正規表現の基本的な機能を利用するためのregex-applicativeのAPIを紹介します。
+まずはただの文字一つにマッチするsym
から:
> match (sym 'a') "a"
+Just 'a'
+
+> match (sym 'a') "b"
+Nothing
sym :: Eq s => s -> RE s s
という型定義のとおり、引数として受け取った文字と文字列における文字が等しいかチェックして、等しければマッチした文字をそのまま返す正規表現を作ります。
また、より一般化したバージョンとして、psym
という関数もあります。
+こちらはpsym :: (s -> Bool) -> RE s s
という型定義のとおり、「文字を受け取ってブール値を返す関数」を受け取って、受け取った関数が文字に対してTrue
を返したらマッチする、という正規表現を作ります。
なので例えば、
+> match (psym (== 'a')) "a"
と書けばsym
関数と全く同じことができますし、
> match (psym (`elem` "abcdef")) "a"
+Just 'a'
+> match (psym (`elem` "abcdef")) "b"
+Just 'b'
と書けば、文字クラスっぽいことができます。
+pure :: a -> RE s a
正規表現に欠かせない、空文字(ε)を表す正規表現も作れます。
+Applicative
型クラスのpure
で表現します。
> match (pure 'a') ""
+Just 'a'
+> match (pure 'a') "b"
+Nothing
もちろん、pure
は任意の値を受け取って「受け取った値をそのまま返すもの」を作ることができるので、結果として文字(列)以外の値を返す正規表現も、簡単に作ることができます。
> match (pure True) ""
+Just True
+> match (pure 42) ""
+Just 42
(*>) :: RE s a -> RE s b -> RE s b
・string :: Eq a => [a] -> RE a [a]
続いて連接、つまり「二つ以上の正規表現を続けてマッチさせる正規表現を作る」処理です。
+regex-applicativeでは、Applicative
型クラスの*>
がそのまま連接として使えるようになっています。
> match (sym 'a' *> sym 'b') "ab"
+Just 'b'
当然、単なる文字の正規表現を並べることはありふれたことなので、string
関数という文字列を渡すだけのバージョンも用意されています。
-- マッチする文字列は同じ、より分かりやすいバージョン
+> match (string "ab") "ab"
+Just "ab"
さらに、regex-applicativeの正規表現オブジェクトはIsString
型クラスのインスタンスでもあるので、OverloadedStrings
言語拡張を使えば文字列リテラルだけで正規表現オブジェクトを作ることができます。
> :set -XOverloadedStrings
+> match "ab" "ab"
+Just "ab"
(<|>) :: RE s a -> RE s a -> RE s a
正規表現の「選択」、すなわち「二つの正規表現のうちどちらか一方にマッチする正規表現を作る」処理は、Alternative
型クラスでおなじみの<|>
を使います1。
> match (sym 'a' <|> sym 'b') "a"
+Just 'a'
+> match (sym 'a' <|> sym 'b') "b"
+Just 'b'
many :: RE s a -> RE s [a]
・some :: RE s a -> RE s [a]
正規表現の「繰り返し」、指定した正規表現を繰り返しマッチさせる正規表現を作る処理は、これまたAlternative
のmany
メソッド・some
メソッドによって実装されています。
+Alternative
型クラスのデフォルトの定義どおり、many
が0回以上の繰り返し、some
が1回以上の繰り返しを表しています。
> match (many (sym 'a')) "aaaaaaaaaa"
+Just "aaaaaaaaaa"
+> match (some (sym 'a')) "aaaaaaaaaa"
+Just "aaaaaaaaaa"
+
+> match (many (sym 'a')) ""
+Just ""
+> match (some (sym 'a')) ""
+Nothing
optional :: RE s a -> RE s (Maybe a)
それから、いわゆる「正規表現の基本三演算」には含まれてませんが(選択とpure
で実装できるので)、この後の例で使用するので「オプショナルなマッチ」を実現する方法を紹介しておきます。
+名前のとおりoptional
という関数を使います。これもAlternative
型クラスに対して使える関数ですね!
> match (sym 'a' *> optional (sym 'b')) "ab"
+Just (Just 'b')
+> match (sym 'a' *> optional (sym 'b')) "a"
+Just Nothing
ここからは、他の正規表現ライブラリーでは珍しい、「正規表現でマッチした結果をHaskellの値に割り当てる方法」をより詳しく紹介します。
+例えば、Text.Regex.Applicative.Common
モジュールにあるdigit
は、一桁の数字(つまり0
から9
)にマッチした上で、結果としてマッチした値を文字ではなく、整数として返す正規表現を提供します。
> match digit "1"
+Just 1
加えて、先ほど紹介したmany
関数と組み合わせると、マッチした結果を整数のリストとして取得することもできます。
> match (many digit) "12345"
+Just [1,2,3,4,5]
(<$>) :: (a -> b) -> RE s a -> RE s b
: 任意の(一引数の)関数を適用するregex-applicativeは、名前のとおり正規表現をApplicativeスタイルで利用できるようにするためのライブラリーです。
+当然ながらApplicative
スタイルに必須の<$>
関数も使用できます。
+正規表現オブジェクトRE s a
型の返す「マッチした結果」に、あなたの好きな関数を適用して変換した正規表現を作れるのです。
先ほどのmany digit
の例を再利用して、マッチした整数の合計値を求めてみましょう。
> match (sum <$> many digit) "12345"
+Just 15
(<*>) :: RE s (a -> b) -> RE s a -> RE s b
: 任意の関数を適用するApplicativeスタイルのもう一つの重要な関数といえば、やっぱり<*>
でしょう。
+many digit
を再利用して、「先頭に書かれた桁数だけ数字を取得する」という例を書いてみます。
> match (take <$> digit <*> many digit) "312345"
+Just [1,2,3]
ここまで紹介した例を使用してもうちょっと複雑な例を書いてみましょう。
+小さな正規表現を組み合わせて、httpかhttpsのURLにおける、オリジンにマッチする正規表現を簡単に書いてみます。
まずは部品作りです。
+URLのスキームにマッチさせるために、「http
の後にオプショナルなs
、続けて://
」という文字列にマッチする正規表現を作ります。
> schemeRe =
+++) <$> string "http" <*> (string "s" <|> pure ""))
+ ((<* string "://"
<*
を使うことで、://
の部分にはマッチしてもマッチした結果は無視している点にご注意ください。
+regex-applicativeはこのように、「マッチしたら関数に渡す文字列」と「マッチしても関数に渡さない文字列」をユーザーが書き分けられるようになっているので、他の正規表現ライブラリーにあるようなキャプチャー2や、先読み言明・後読み言明などの機能が必要ないのです。
続けて、ホスト名にマッチする正規表現を作ります。
+ここでは単純化して、「アルファベットの小文字かピリオド1文字以上」という文字列にしておきます。
> hostRe = some (psym (`elem` ['a'..'z'] ++ "."))
最後はポート番号です。
+:
という文字の後にText.Regex.Applicative.Common
に入ったdecimal
、すなわち一桁以上の10進数にマッチする正規表現を使います。
> portRe = sym ':' *> decimal
以上で正規表現のパーツができました。
+早速使ってみる… 前に、マッチした結果を割り当てるレコード型を定義します。
data Origin =
+Origin { scheme :: String, host :: String, port :: Int }
+ deriving Show
あとは<$>
や<*>
を使って組み合わせて、Origin
値コンストラクターに食わせるだけです!
+ポート番号はオリジンにおいてはなくても良いので、省略した場合は仮に80
としておきましょう3。
= Origin <$>
+ originRe <*>
+ schemeRe <*>
+ hostRe <|> pure 80) (portRe
今度こそ使ってみます。
+> match originRe "https://example.com:8080"
+Just (Origin {scheme = "https", host = "example.com", port = 8080})
+> match originRe "http://example.com"
+Just (Origin {scheme = "http", host = "example.com", port = 80})
regex-applicativeを使うことで、URLのオリジンにマッチさせるだけでなく、マッチした結果をOrigin
型の値として割り当てる正規表現が作れました!🎉
regex-applicativeパッケージには、他の正規表現ライブラリーと比べて、以下のメリットがあります。
+一方regex-applicativeパッケージには、他の正規表現ライブラリーに対する以下のデメリットがあります。
+String
以外の文字列にはマッチできない…
+Text
やByteString
向けのものも、原理的に実装できないというわけではないはずここからは、regex-applicativeにおける正規表現エンジンがどのように作られているか、『正規表現技術入門』における正規表現エンジンの分類を参考に説明しましょう。
+『正規表現技術入門』のp.56では、正規表現エンジンを次の二つに分類しています。
+さて、regex-applicativeの場合はどちらなのでしょうか?
+ソースコードを読んでみると、どうやらどちらでもなさそうなことがわかります。
+というのも、正規表現オブジェクトRE s a
をNFAにcompile
という関数で変換した後、DFAに変換しないでそのまま使っているからです。
+一般的に、NFAはDFAに変換可能で、変換してからマッチさせた方がしばしば高速にマッチできることが知られています。
+ところがregex-applicativeではその変換を行わず、NFAとして使用しているのです。
なぜそうした仕様になっているかについて、私の推測を述べましょう4。
+regex-applicativeでは先ほど紹介したpsym
関数のように、「任意の文字を受け取る関数」を正規表現オブジェクトに含められなければなりません。
+結果、関数がどんな文字の時にどんな値を返すのか(マッチが成功するのかしないのか)、正規表現オブジェクトをコンパイルする関数にはわからなくなってしまうのです。
+一方、効率の良いDFAの実装では、DFAの一つ一つの状態ごとに「どの文字を受け取ったら次はどの状態に遷移するか」という情報を、連想配列として持っておかなければなりません5。
+そのため、どの文字を受け取ったらマッチが成功するのかわからない箇所が正規表現オブジェクトに混ざっている限り、効率の良いDFAの実装にはできないのです。
その分、regex-applicativeでは任意の文字を受け取る関数が使えるので、普通の正規表現ライブラリーよりも柔軟に書くことができるようになっています。
+その点を考慮したトレードオフなんでしょう。
さらにregex-applicativeの実装を掘ってみましょう。
+先ほど紹介したcompile
関数は、正規表現オブジェクトRE s a
をReObject s r
という型の、Thread s r
型の値のキューに変換します。
+これがregex-applicativeにおけるNFAと呼べそうですね。
newtype ReObject s r = ReObject (SQ.StateQueue (Thread s r))
Thread s r
型の値は、NFAにおける状態遷移を表します。
data Thread s r
+= Thread
+ threadId_ :: ThreadId
+ { _threadCont :: s -> [Thread s r]
+ ,
+ }| Accept r
型定義のとおり、Thread
とAccept
という二通りの値をとります6。
Thread
はその用途からして、事実上s -> [Thread s r]
という関数と同等の型です。regex-applicativeはReObject
によって文字列[s]
の値をマッチさせる際、このs -> [Thread s r]
に文字を渡します。
+Thread s r
型の値を(そのリストから)一つずつキューに追加して、また次の文字にマッチさせます。Thread s r
の値を取り出して(値コンストラクターがThread
であれば)マッチしなかった文字をまたs -> [Thread s r]
に渡します。threadId_
はキューに追加する際同じthreadId_
のThread
を追加してしまうのを防ぐためのキーです。詳細は割愛します。Accept r
は名前のとおりNFAの受理状態を表しています。s -> [Thread s r]
を繰り返し適用して最終的にAccept r
を返したThread
のみが「マッチした」と解釈されます。このように、regex-applicativeにおけるNFAはs -> [Thread s r]
を返す関数、すなわち「文字を受け取って次の状態のリストを返す継続」として作られています。
ただ実際に実行する際の流れを見てみると、ReObject
に含まれるThread
を一つずつ実行してみて、結果が条件に合うものを選ぶ、といった方が近いです。
+例えばmatch
関数では、ReObject
に文字を一文字ずつ与えた結果の中から、listToMaybe
を使って最初にAccept
にたどり着くThread
を取得しています。
+それから、最長マッチする部分文字列を検索するfindLongestPrefix
関数は、マッチが失敗するか残りの文字列が空になるまで繰り返し文字をReObject
に与えることで、できるだけ長いマッチが返るように調整しています。
+このようにregex-applicativeは、ReObject
(NFA)に文字を一つずつ与えてマッチ結果を生成する処理と、そのマッチ結果を選び取る処理とを分離することで、様々な方針でマッチできるようになっているのです。
さて、ここまでこの文章を読んでいただけた方の中には、「これってmegaparsecとかattoparsecとかのパーサーコンビネーターライブラリーと何が違うんだ?」という疑問をお持ちの方も多いでしょう。
+そう、大抵の場合、パーサーコンビネーターライブラリーも下記のような特徴を持ち合わせています。
Applicative
やAlternative
型クラスのメソッドを利用したAPIChar
)」の列以外にもマッチできる特に「Applicative
やAlternative
型クラスのメソッドを利用したAPI」である点は興味深く、場合によっては、使うライブラリーだけ換えて式をコピペしてもコンパイルは通る、なんてことが普通にあり得るくらい似ています。
+ただし、当然コンパイルが通るだけでは意図通りに動くとは限りません。
+regex-alternativeと一般的なパーサーコンビネーターライブラリーには、「自動的にバックトラックをするかしないか」という違いがあるためです。
例えば、次の式はregex-applicativeでもattoparsecでも有効な式ですが、regex-applicativeのmatch
関数では、「ab
が1回以上繰り返される文字列」にマッチして最後のab
を返すことができるのに、attoparsecのparse
関数ではパースに失敗してしまいます。
"ab") *> string "ab" many (string
stack build regex-applicative attoparsec
した上で以下のように書いて試してみましょう。
+まずはregex-applicativeで試す場合:
> import Text.Regex.Applicative
+
+> match (many (string "ab") *> string "ab") "abab"
+Just "ab"
+
+> match (many (string "ab") *> string "ab") "ab"
+Just "ab"
+
+> match (many (string "ab") *> string "ab") "ababab"
+Just "ab"
いずれの文字列でもJust "ab"
が返ってきてますね😌。
続いてattoparsecで試す場合:
+-- attoparsecは`String`をサポートしてないのでOverloadedStringsでTextとして扱う
+> :set -XOverloadedStrings
+
+> import Control.Applicative
+> import Data.Attoparsec.Text
+
+-- 文字列の終端であることを明確にするために、空文字列をfeedしておく
+> feed (parse (many (string "ab") *> string "ab") "abab") ""
+Fail "" [] "not enough input"
+
+> feed (parse (many (string "ab") *> string "ab") "ab") ""
+Fail "" [] "not enough input"
+
+> feed (parse (many (string "ab") *> string "ab") "ababab") ""
+Fail "" [] "not enough input"
いずれの文字列でも失敗になってしまいました。なぜうまくいかないのでしょう?
+それは文字列"ababab"
におけるab
を、many (string "ab")
が消費してしまい、*>
の右辺に書いたstring "ab"
が処理できなくなってしまうためです。
+対するregex-applicativeにおけるmany (string "ab") *> string "ab"
では、正規表現全体がマッチするよう、自動でバックトラックしてくれます。
+regex-applicativeでも最初にmany (string "ab")
が"ababab"
全体を消費した直後では、*>
の右辺に書いたstring "ab"
のマッチは当然失敗してしまいます。
+しかし、regex-applicativeはそれではあきらめません。*>
の右辺に書いたstring "ab"
が成功するまで、失敗する度にmany (string "ab")
が消費した文字を1文字ずつ返却してくれるのです。これがバックトラックです。
+regex-alternativeに限らず、大抵の正規表現エンジンがこのように自動的なバックトラックを行います。
こうした性質の違いにより、regex-applicativeは文字列の中間に指定したパターンをマッチさせるのが、パーサーコンビネーターライブラリーよりも得意です。
+例えば「文字列の中間にある1桁以上の10進数」にマッチさせる場合、regex-alternativeでは次のように書きます。
+> import Text.Regex.Applicative.Common
+> match (few anySym *> decimal <* few anySym) "abc12345def"
+Just 12345
few
は「控えめな繰り返し」を実現するための関数です。引数で指定した正規表現を0回以上マッチさせる、という点ではmany
と同じですが、前後にある正規表現がなるべく長くマッチするよう、優先してマッチさせてくれます。
+few anySym
は普通の正規表現ライブラリーでいうところの.*?
に相当します。
同じことをattoparsecで実現するためにmany anyChar *> decimal <* many anyChar
と書いてみても、やはりうまくいきません。
> import Data.Attoparsec.Text
+
+> feed (parse (many anyChar *> decimal <* many anyChar) "abc12345def") ""
+Fail "" [] "not enough input"
理由は先ほどと同様で、最初に書いたmany anyChar
がすべての文字列を消費してしまい、それ以降のdecimal
などがマッチできないためです。
+正しく処理するには、「decimal
の先頭以外の文字列」、すなわち「数字以外の文字列」がmany
であることを明示する方法をとるしかありません7。
> import Data.Char
+
+> nonDigits = many (satisfy (not . isDigit))
+> feed ((parse (nonDigits *> decimal <* nonDigits)) "abc12345def") ""
+Done "" 12345
そんなわけで、regex-applicativeは、HaskellによくあるパーサーコンビネーターのようにApplicativeスタイルで書けて、なおかつ他の正規表現ライブラリーのように中間マッチがしやすいという、両方の良さを持ち合わせていると言えます。
+…と、regex-applicativeのよさを語ったところで舌の根も乾かぬうちに恐縮ですが、実はattoparsecをはじめパーサーコンビネーターライブラリーの「中間マッチがやりにくい」という弱点を改善するためのパッケージがあります。
+replace-attoparsecやreplace-megaparsecといいます8。
+名前のとおりreplace-attoparsecがattoparsecを改善するパッケージで、replace-megaparsecがmegaparsecを改善するパッケージです。
+名前もAPIもお互いそっくりなんで(作者も同じですしね)、今回はreplace-attoparsecの方を紹介しましょう。
replace-attoparsecを使えば、次のように書くだけで「文字列の中間にある1桁以上の10進数」を取り出すことができます。
+import Replace.Attoparsec.Text
+
+> feed (parse (sepCap decimal) "abc12345def") ""
+Done "" [Left "abc",Right 12345,Left "def"]
+"abc12345def"
の中間にある12345
だけでなく、パースできなかったabc
、def
という文字列もおまけで取得できました!
+decimal
がパースできた箇所がRight
として、パースできなかった箇所がLeft
として返却されていることに注意してください。
replace-attoparsecのsepCap
(「Separate and Capture」の略だそうです)は、引数として受け取ったパーサーを、
という処理を繰り返しています。
+結果的にパースできない文字列はすべてスキップして、文字列の中間にある、パースできる文字列のみにパーサーを適用できるのです。
そろそろ力尽きてきたのでここからはスライドのコピペで失礼します…🙏
+以上です!👋
+まとめもスライドからのコピペで!
Alternative
は、Applicative
より強力な(できることが多い)型クラスです。そういう意味で、regex-applicativeは本当は「regex-alternative」と呼んだ方が適切なのかも知れません。↩︎
正確には、キャプチャーした文字列を正規表現の中で再利用することができないので、他の正規表現ライブラリーのキャプチャー機能と完全に同等のことができるわけではありません。これは現状のregex-applicativeの制限です。↩︎
もちろん、実際のところhttpsの場合デフォルトのポート番号は443であるべきですが、ちゃんと実装しようとすると結構複雑になるのでご容赦を!↩︎
この記事の最後の方を書いていて思い出しました。regex-applicativeはDFAベースの正規表現エンジンでは不可能な「控えめな繰り返し」をサポートしているから、という理由もあるようです。なぜDFAベースでは「控えめな繰り返し」ができないかは私もうまく説明できません…。↩︎
『正規表現技術入門』のp. 132における実装例では、これを状態と文字による二次元配列として実装しています。↩︎
並行並列プログラミングで出てくるあの「スレッド」とは違うのでご注意ください。↩︎
ただし、一般に、正規表現ライブラリーであってもこのような書き方をした方が効率よくマッチさせやすいでしょう。↩︎
こちらの記事でも触れているとおり、かつて私も同じ目的のパッケージを作成しました。しかし、これらのパッケージの方が明らかにドキュメントが充実していて、機能も豊富なので今回はこれらを紹介します。将来的にはsubstring-parserはdeprecatedにするかも知れません。↩︎
先日、我らがHaskellのデファクトスタンダードなコンパイラー、GHCのバージョン8.8.1-alpha1がリリースされました。
+このリリースはまだアルファ版であることからわかるとおり、主にテスト目的で使用するためのものです。
+なのでいち早く試してみて、GHCのデバッグに貢献してみましょう。
そこで今回は、最近Haskellを始めた方なら使っている方も多いであろう、stackを使ってこの新しいGHCをインストールし、あなたのライブラリー・アプリケーションでテストする方法を紹介いたします。
+いきなりやろうとすることを真っ向から否定するようで恐縮ですが…😅
+実際に私が試しにビルドしてみた感じ、普通にcabal-installをこちらからインストールして、cabal new-build --with-ghc=ghc-8.8.0.20190424
などと実行した方がいいんじゃないかという気がしました…。
+cabal-installにはGHCをインストールする機能はないので、その場合はGHCは別途インストールすることになります(ghcup
が使える?)。
+@takenobu-hsさんが書いてくれた、こちらの記事を参考にどうぞ!
なお、stackでやると面倒な理由についての詳細はこれから述べる手順で適宜触れます…。
+setup-info
を作るまずはじめに、stackがGHCをインストールする際に参照する、setup-info
というYAMLを作りましょう。
+setup-info
はstack setup
やstack build
を実行したとき、GHCなどの必要なソフトウェアがインストールされていなかった際、自動でGHCをインストールするために必要な情報です。
+GHCのバージョンや対象となるプラットフォームごとに、GHCのビルド済みtarballへのURLやそのチェックサムが書いてあります。
+stackはここに書かれたURLにアクセスすることで、GHCをインストールしているんですね。
デフォルトでは、stackはこちらのYAMLファイルをsetup-info
として扱っています。
+このYAMLにはStackageが参照している、安定版のGHCについては書いてあるものの、LTS HaskellにもStackage Nightlyにもまだ採用されていないGHCについては、書かれていません。
+当然アルファ版であるGHC 8.8.1-alpha1が書かれることはないため、GHC 8.8.1-alpha1用のsetup-info
を作る必要があります。
それでは書いてみましょう… と、言いたいところですが、このsetup-info
、実際のところ自分で直接書く必要はなく、YAMLファイルへのURLやパスを指定するだけでstackは参照しに行ってくれます!
+と、言うわけで、こちらにGHC 8.8.1-alpha1向けのsetup-info
を作ってアップロードしておきました!
+(申し訳なくもLinuxについてはどう書けばいいかわからず、macOSとWindows 64bitのみ対応いたしました… あしからず。🙇)
ひとまずみなさんは、下記のいずれかの方法で指定するだけでこの手順はクリアできます。
+stack.yaml
に記載する:
resolver: ghc-8.8
+setup-info: "https://gist.githubusercontent.com/igrep/7298e1e2515059ae332feaf5501c41a4/raw/d69cc0b75d9be6735bdfcca6aa3eb6398d98983f/stack-setup-info.yaml"
+# ... 以下略 ...
stack.yaml
を置きましょう。stack exec ghci
などと実行すればOKです!stack setup
コマンドのオプションとして渡す:
stack setup 8.8.0.20190424 --setup-info-yaml https://gist.github.com/igrep/7298e1e2515059ae332feaf5501c41a4/raw/d69cc0b75d9be6735bdfcca6aa3eb6398d98983f/stack-setup-info.yaml
+--setup-info-yaml
オプションを指定した上で8.8.0.20190424
という引数を与えるのがポイントです。8.8.1-alpha1
ではなく8.8.0.20190424
となっている点に注意してください!「8.8.1-alpha1じゃなくて、自分でビルドしたGHCをstack
でインストールできるようにしたい!」というマニアなあなたは、今回私が作ったsetup-info
をどうぞ参考にしてください!🙇
ここからは、何かしら依存するパッケージがあるライブラリー・アプリケーションをGHC 8.8.1-alpha1で試しにビルドしたいという方向けです。
+GHC 8.8.1-alpha1をちょっと試したいだけという方はこれ以降を読む必要はありません。
まずは、ひとまず対象となるプロジェクトのstack.yaml
に
allow-newer: true
を追記しましょう。
+これは、依存しているCabalパッケージのバージョンの、上限を取っ払うというものです。
+依存パッケージのバージョンの上限は、パッケージの開発者が自身のパッケージを確実にビルドできるよう、「このパッケージはあのパッケージのバージョンN.M以下じゃないとビルドできないよ!」とCabalの依存関係リゾルバーに教えてあげるためのものです。
+cabal-install(と、恐らくstackも必要に応じて)は、通常であればこの上限を見て、どのバージョンのパッケージをインストールするか決めます。
+その上限により、残念ながら依存関係の解決に失敗することがあるのです。
+そこでそうしたエラーを避けるためにもallow-newer: true
と設定して、上限を無視してみましょう。
というのも、このバージョンの上限はしばしば、予防のために実際より厳しめに設定されることがあるためです1。
+そりゃそうですよね。今作っているパッケージが依存しているAPIが、どのバージョンで使用できなくなるかなんて、大抵のパッケージではわかりませんし。
+Haskellの世界にはPVPという、Semantic Versioningと似た思想のバージョン変更ポリシーがありまして、APIの互換性がなくなるような修正が含まれる場合、次のバージョンではA.B.C
のA.B
の箇所を変更することになっています。
+これを信じて依存バージョンの上限(と下限)を設定してみても、実際にあなたが依存しているAPIが使用できなくなるとは限らないのです。
したがって、依存パッケージのバージョンの上限は、実際には無視してもよい場合がしばしばあります。
+もちろん、自分で依存パッケージのバージョンを正しく書き換えて対応するというのもアリですし、将来的にはそうした方がより望ましいやり方です。
+また、allow-newer: true
を設定することにより、GHC 8.8とは関係のない原因でビルドが失敗する可能性がある点にも注意してください。
+とは言え、今回は手っ取り早くビルドしてみるために、敢えてallow-newer: true
を設定することと致しました。
+「私はバージョンの上限を直してみたいんだー!」という方は、是非チャレンジしてみてください。
stack.yaml
に書いておいた方が良い設定がもう一つあります。
+それは、HEAD.hackageの設定です。
これからビルドするあなたのパッケージは、きっとたくさんのパッケージに依存していることでしょう。
+残念ながら、その中にはGHC 8.8に対応できていないものも数多くあるでしょう😰。
+特に今回はMonadFail
Proposalによる、Monad
型クラスの仕様変更を適切に周知できていなかったこともあり、まだ多くのパッケージが対応できていないようです。
しかし、まだ希望はあります。
+あなたの依存パッケージに対する必要な修正は、すでにmasterブランチにマージされているかも知れませんし、すでに誰かがPull requestを送っているかも知れません。
+さらにラッキーな場合、HEAD.hackageにパッチを当てたバージョンが上がっていることでしょう!
HEAD.hackageは、今回のようにGHCの開発版をいち早く試したい人が、新しいGHCに向けて修正を加えたパッケージを、いち早くアップロードするサイトです。
+こちらのリポジトリーにパッチをアップロードすることで、cabal-installやstackから、普通のhackageにあるパッケージとしてダウンロードできるようにしてくれます。
HEAD.hackageをstackで利用するには、下記のように、package-indices:
という設定を、stack.yaml
に加えてください。
+下記のように記載することで、stackは、HEAD.hackageにある修正済みのパッケージを優先して取得してくれるようになります2。
package-indices:
+ - name: head.hackage
+ download-prefix: http://head.hackage.haskell.org/package/
+ http: http://head.hackage.haskell.org/01-index.tar.gz
+ - name: Hackage
+ download-prefix: https://hackage.haskell.org/package/
+ http: https://hackage.haskell.org/01-index.tar.gz
これでGHC 8.8対応済みのパッケージを、簡単に取得できるようになります!
+ここまで設定できたら、いよいよstack build
してみましょう3!
+とは言え、この状態ではほぼ間違いなく失敗が続くので、stack build --file-watch
と、--file-watch
オプションを付けて、stack.yaml
を編集する度に再度ビルドが実行されるようにするのをおすすめします。
と、言うのも、恐らく次👇のようなエラーがたくさん出ると思われるからです。
+...
+In the dependencies for wss-client-0.2.1.1:
+ http-client must match >=0.5.13, but the stack configuration has no specified version (latest
+ matching version is 0.6.4)
+ http-client-tls needed, but the stack configuration has no specified version (latest matching
+ version is 0.3.5.3)
+ network-uri needed, but the stack configuration has no specified version (latest matching
+ version is 2.6.1.0)
+ websockets must match >=0.12.0 && <0.13, but the stack configuration has no specified version
+ (latest matching version is 0.12.5.3)
+needed since wss-client is a build target.
+
+Some different approaches to resolving this:
+
+ * Consider trying 'stack solver', which uses the cabal-install solver to attempt to find some
+ working build configuration. This can be convenient when dealing with many complicated
+ constraint errors, but results may be unpredictable.
+
+ * Recommended action: try adding the following to your extra-deps
+ in C:\Users\igrep\Downloads\direct-hs\stack-ghc-8.8.yaml:
+
+attoparsec-0.13.2.2@sha256:6a0baba19991e84ef939056e7b411ad3a1ea0fb5e1e8fce7ca50e96c84b206c8
+base-compat-0.10.5@sha256:d49e174ed0daecd059c52d13d4f4de87b5609c81212a22adbb92431f9cd58fff
+...
+このエラー、見かけたことがある人も多いでしょう。
+そう、指定したresolver(stackが使用するパッケージのバージョンの一覧。Stackageに登録されているlts-13.12
などもその一つ)に、必要なバージョンのパッケージが登録されていない場合に起こるエラーです。
+みなさんが普段利用するlts-13.12
などのresolverでは、数多くのパッケージが登録されています(最新版のLTS Haskell 13.19で2346件。Stackageをメンテしている皆さんのおかげですね)。
一方、最初の手順で我々が指定したresolver、すなわちresolver: ghc-8.8
は、GHC 8.8に添付されたパッケージ(base
パッケージや、array
パッケージなど)しか入っていない、実質空っぽなresolverなのです(参考)。
+そのため、あなたが必要なほとんどのパッケージはないため、stackはやむなく「extra-deps
にこれらのパッケージを追加してね!」というエラーを出すことになります。
+これではstackの良さを生かせません…。cabal-installでcabal new-build
していれば、cabal-installは黙って必要なパッケージのバージョンを決定し、あとはcabal new-freeze
でもすれば、完全にビルドを再現可能な状態にしてくれます。
+やっぱりstackはあくまでもStackageを活かすためのツールと捉えた方がいいのかも知れません😥。
extra-deps
へのパッケージの追記を何度か繰り返すと、ようやくパッケージのビルドが始まります。
+HEAD.hackageに収録されたパッケージを正しく取得できていれば、現在Hackageにアップロードされているバージョンではビルドできない依存パッケージも、無事ビルドできることでしょう。
+依存するパッケージの数にもよりますが、やっぱり時間がかかるかと思います。待ちましょう☕️。
extra-deps
を使い倒すしかしやっぱり、必要な変更が施されたパッケージが、HEAD.hackageにもアップロードされていない場合はあります。
+そうした場合、自分で修正して(Pull requestを送りつつ)パッチをHEAD.hackageのリポジトリーにアップロードすることもできますが、stack.yaml
のextra-deps
を次のように使えば、もっと手っ取り早く修正したバージョンのビルドを試すことができます。
自分以外の人が対象のパッケージを修正したので、すでにどこかのリポジトリーにpush済みのコミットがある、という場合、下記👇のように書くと、Gitリポジトリーの特定のコミットを直接参照した状態で、依存関係に加えることができます。
+extra-deps:
+- git: https://github.com/github_user/repository_name.git
+ commit: <修正したコミットのSHA>
そうでない場合、対象のパッケージのリポジトリーを一旦git submodule add
して、自分のリポジトリーの一部に含めてしまいましょう。
+その上で、extra-deps
には下記のように書けば、stackはローカルのファイルシステムに置かれたディレクトリーも、直接依存するパッケージとして追加してくれます。
extra-deps:
+- ./path/to/package
逐一別のディレクトリーにgit clone
してgit commit
してgit push
して作られたコミットのSHAを参照して… なんてのを繰り返していたら、面倒だからです。
臨機応変に対応しましょう…😰
+ちなみに、extra-depsのドキュメントいわくstackはMercurialもサポートしています。
以上がstackを使ったGHC 8.8-alpha1のインストール方法や、それを利用したパッケージのビルド手順です。自分でGHCをビルドしたときなども参考にしてみてください。
+これで終わり…!と、言いたいところですが、GHC 8.8に関連して、非常に意欲的なプロジェクト💪を紹介させてください。
それは、Operation Vanguardです。
+@fumievalさんが始めた、「エコシステムの主要なパッケージの最新版を一挙にGHC 8.8に対応させる」プロジェクトです。
+一旦submoduleとして対象のパッケージのリポジトリーをcloneする、という方法は、Operation Vanguardのリポジトリーを見ていて知りました💡。
すでに対応のほとんどが終了したとのことですが、GHC 8.8に対応していないパッケージは恐らくまだたくさんあります。
+ゴールデンウィークももう半分が終わりましたが、時間をとってOperation Vanguardのようにチャレンジしてみるのはいかがでしょうか💪💪💪
もっとも、私のようにものぐさな人間が作るパッケージには、そもそも上限も何も書いてないことが多いのですが…😰↩︎
本来であればHackage Securityの設定も必要なはずなんですが、なぜかうまくいかず…😱。こちらで紹介されたworkaroundにしたがって、関連する設定を除くことにしました…。↩︎
stack solver
コマンドを使えば、この節で紹介するエラーは簡単にクリアできそうだということを聞いて試した(Thanks, @mizunashi-manaさん!)のですが、手元のパッケージでは依存関係を解決できず、エラーになってしまいました…。↩︎
多くの言語では, here document (heredoc) という言語機能が搭載されています.これは,複数行の文字列をコード中に文字列リテラルとして埋め込める機能です.今日は heredoc ほど使い勝手がよくないものの,長い文字列を埋め込める, Haskell 標準の string gap という機能を紹介したいと思います.
+bash では,複数行の文字列を,次の記法で埋め込むことができます:
+echo "$(cat <<EOS
+some text
+is multilined
+EOS
+)"
これは,
+some text
+is multilined
+という文字列が出力されます.多くの言語では似たような構文で heredoc が採用されていて,特殊な記号の後に終端記号を書いて,その後の終端記号までを文字列リテラルとして扱われます. Haskell では残念ながらこのような機能は搭載されていませんが,代わりに次の記法が提供されています:
+main :: IO ()
+= putStrLn "\
+ main \some text\n\
+ \is multilined\
+\"
この実行結果は,前の bash スクリプトの結果と同じになります. heredoc より色々ごちゃごちゃしてますが,複数行の文字列リテラルを書けます.この機能は, Haskell の複数行文字列リテラルまたは Haskell 標準では gap と呼ばれています 1.記法はかなり単純で,文字列中のバックスラッシュ \
で囲まれた空白が無視されるだけです.改行も空白に含まれます.なので,上のプログラムは以下のプログラムと同じです:
main :: IO ()
+= putStrLn "some text\nis multilined" main
なお, gap を使わないで複数行の文字列リテラルを書くことはできません.また, gap は空白を全て無視するため,改行を含まない長い文字列を複数行に渡って埋め込むのにも使えます:
+main :: IO ()
+= putStrLn "This is very very very \
+ main \long long long long long long long long text."
なお, gap は Haskell 標準でレイアウトルールの処理から除外されているため2,インデントを考慮する必要はありません:
+main :: IO ()
+= do
+ main putStrLn "one line"
+ putStrLn "\
+ \multiline\n\
+\text\
+\"
ただ, GHC の CPP
拡張を使用する際注意が必要です. CPP
では,バックスラッシュで終わる行は,バックスラッシュを除いて次の行と繋げる処理が行われます3.この処理のため, gap を使用した以下のコードは,
{-# LANGUAGE CPP #-}
+
+main :: IO ()
+= putStrLn "This is very very very \
+ main \long long long long long long long long text."
cpp
により次のように変換されてしまいます:
main :: IO ()
+= putStrLn "This is very very very \long long long long long long long long text." main
このため,結果的にコンパイルエラーになってしまいます.このため, CPP
を使う際は, gap を使わず CPP
の機能を使う必要があります.例えば,上記のプログラムは,
{-# LANGUAGE CPP #-}
+
+main :: IO ()
+= putStrLn "This is very very very \
+ main \ \long long long long long long long long text."
と書くと gap をそのまま使った時のプログラムと同じになります.一番最初の \
は CPP
のためのもの,次の 2 つは gap になります.
string gap は,昔から Haskell 標準で付いている機能なので,ぜひ使ってみてください.
+ただ, heredoc より使い勝手は良くないです.変数展開やもう少し見栄えの良い heredoc が欲しい場合は, here パッケージ や Shakespeare などの TemplateHaskell を使ったテンプレートエンジンの使用を検討してみるといいかもしれませんね.
+では,今日はこれでノシ
+Haskell2010 の 2.6 節の最後の方で紹介されています.↩︎
Haskell2010 の 10.3 節 で触れられています.↩︎
CPP の仕様の 1.2 節 で触れられています.↩︎
現職でHaskellを仕事で書き始めるようになってからというもの、度々小さなパッケージをリリースするようになりました。
+敢えてパッケージにするほどのものでもなさそうなぐらい小さなものが多いですが、もし再利用したくなったらな、という気持ちで書いております。
strip-ansi-escapeというパッケージです。
+今回もメインの処理は100行にも満たないような小さなもので、また用途もニッチです。
+具体的には、名前のとおりANSIエスケープコードを文字列から取り除く、ただそれだけです。
+使い方も極めてシンプル:
> import Data.String.AnsiEscapeCodes.Strip.Text
+ ghci
+-- 現状Text型向けにしか作っていないため、OverloadedStringsを有効にした方が使いやすい
+> :set -XOverloadedStrings
+ ghci> import Data.Text
+ ghci
+-- 出力すると下線付きで "hello" と表示されるANSIエスケープコード付きの文字列
+> "\x001B[4mhello\x001B[0m"
+ ghci"\ESC[4mhello\ESC[0m"
+
+> stripAnsiEscapeCodes "\x001B[4mhello\x001B[0m"
+ ghci"hello"
通常我々がANSIエスケープコードを扱うときは、ユーザーのために端末に文字列を分かりやすく表示したいときで、それをプログラムで再利用することは想定していません。
+そのためANSIエスケープコードを出力できるアプリケーションは、大抵の場合出力しないよう設定できる(あるいは、出力先がttyでないことを検出して出力しない)ようになっています。
+なので、プログラムがANSIエスケープコードの混ざった文字列を扱わざるを得ない、という事態は、何かがおかしい事態だと言えるでしょう。
一体どういう事態なのかというと、それは私がずっと開発中の、対話的Haskell入門コンテンツ — 「失敗しながら学ぶHaskell入門」 — で出遭った事態でした。
+「失敗しながら学ぶHaskell入門」(以下、英語名を略して「mmlh」と呼びます)では、ユーザーが書いたHaskellのソースコードを受け取って、GHCにコンパイルさせることで、型エラーなどのエラーメッセージを取得しています。
+当初からmmlhはそれを簡単にパースしてユーザーへのヒントを出すのに使ったり、ユーザーにそのまま表示したりするのに使うため、-fdiagnostics-color=always
というオプションをGHCに渡していました。
+これは、エラーメッセージに色を着けるようになったGHC 8.2から導入されたオプションで、「エラーメッセージに必ず(ANSIエスケープコードを使って)色を着ける」というものです。
+GHCが出すエラーメッセージを「簡単にパース」しつつ「ユーザーにそのまま表示」する、という2つの要件を満たすためには、このオプションを利用して、強制的にエラーメッセージに色を着ける必要がありました。
さらに最近、GHCが出したエラーメッセージをファイルに保存して、GitHubで閲覧できるようにする(正確には、閲覧して各行にコメントできるようにする)、という機能も追加した結果、ANSIエスケープコードを取り除かざるを得なくなってしまったのです。
+というのも、-fdiagnostics-color=always
を有効にしている限り、GHCは必ずANSIエスケープコードをエラーメッセージに混ぜるので、ファイルに保存してGitHub上で表示する際、下記のように余計な文字として混ざってしまい、エラーメッセージが読みづらくなってしまうためです。
�[;1m16.hs:19:18: �[;1m�[31merror:�[0m�[0m�[;1m�[0m�[0m�[;1m
+ • No instance for (Num ([Char], String))
+ arising from a use of ‘countWords’
+ • In the expression: countWords (concat wordsList)
+ In an equation for ‘countMap’:
+ countMap = countWords (concat wordsList)
+ In the expression:
+ do paths <- getArgs
+ wordsList <- for paths scrapeWords
+ let countMap = countWords (concat wordsList)
+ for_ (toList countMap) catCount�[0m�[0m
+�[;1m�[34m |�[0m�[0m
+�[;1m�[34m19 |�[0m�[0m let countMap = �[;1m�[31mcountWords (concat wordsList)�[0m�[0m
+�[;1m�[34m |�[0m�[0m�[;1m�[31m ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^�[0m�[0m
+�[0m�[0m�[0m
+-fdiagnostics-color=always
を有効にしなければこんな問題は起こらないのですが、そうすると今度はユーザーにエラーメッセージを表示させる際、色が着かなくなってしまいます。
+せっかくGHC 8.2以降を使っているのに色つきのエラーメッセージが見られないのは残念ですよね。
+GHCを2回実行することで、ユーザーに表示する用のエラーメッセージとファイルに保存する用のエラーメッセージを分けることもできますが、それでは効率が悪いでしょうし。
そんなわけで、GHCが出力するエラーメッセージをユーザーに端末上で表示する用途と、ANSIエスケープコードを解釈しない箇所で表示する用途、両方に使用したくなったため、今回敢えてANSIエスケープコードを取り除くライブラリーを作りました。
+もし他に同じような事態に出遭った方がいらっしゃいましたら、試してみてください🙏
ついでにここ数ヶ月弊社でやっている、mmlhを使った社内勉強会のお話も書こうかと思いましたが、やっぱり社内でのことなんで、会社のブログに書くことにします。
+多分今週中には上げますので乞うご期待!
+⬇️大分遅くなってしまいましたが公開しました!
先日teratailのHaskellタグを眺めていたところ、下記のような質問がありました。
+Haskell - networkパッケージがうまく機能しない|teratail
+TidalCyclesという、Haskell製の内部DSLでシンセサイザーの演奏ができるライブラリーのインストールがうまくいかない、という質問です。
+networkパッケージがインストールできていない、ということなのでcabal hellにでもハマったのかな、と思ったのですが、類似しているとおぼしき報告(と、こちら)を読む限り、どうもGHCのインストール自体に何か問題があるように見えました。
もう当の質問者はHaskell Platformをインストールすることで解決したそうですが、いずれにしても、我々Haskellerとしては、stackなりcabal new-installなりといった、慣れた方法でインストールできた方がサポートしやすいですし、きっと確実です。
+というわけで今回はstackでのインストールに挑戦してみました。
+すでにstackをインストールしているというHaskell開発者は多いでしょうし、そうした方がTidalCyclesを使いたくなったときの参考になれば幸いです。
結論から言うとほとんど問題なくできたんですが、以下のtweetで述べたポイントにご注意ください。
+++ ++TidalCycles, Atomのpackageの設定でstack exec ghciを使うよう設定したら普通にstackで入れたGHCから使えましたわ。
+— Yuji Yamamoto: 山本悠滋 (@igrep) 2019年1月8日 +
ポイントは、
- hosc-0.17のstack.yamlのextra-depsに加えないといけない
- WindowsでGHC 8.6.3は地雷なのでLTS 12.26を使う
- ~/.ghciで:set +mしてるとうまく動かない
ぐらいか。 +
stack --version
: Version 1.9.1, Git revision f9d0042c141660e1d38f797e1d426be4a99b2a3c (6168 commits) x86_64 hpack-0.31.0冒頭に挙げた質問をされた方が参考にしたページ TidalCyclesのインストール2018年版 - Qiita では、Chocolateyを使ったインストール方法を紹介していますが、この方法では、直接GHCのWindows向けtarballをダウンロードしてインストールしているようです。
+私が知る限り特にその方法でも問題はないはずなんですが、なぜか質問者が挙げたようなエラーが発生してしまいます。
+また、TidalCyclesが実行時に依存しているSuperColliderやSuperDirtといったソフトウェアを、別のChocolateyのパッケージに分けることなく、TidalCyclesのインストールスクリプトで直接インストールしているようです(詳細はChocolateyのパッケージ情報に書かれたchocolateyinstall.ps1を参照されたし)。
+そのため、ChocolateyでTidalCyclesをインストールしようとすると、問題のあるGHCと、SuperColliderなどの依存パッケージを一緒にインストールしなければなりませんし、SuperColliderやSuperDirtだけをChocolateyでインストールすることもできません。
なので、ここは素直にTidalCycles公式のWikiに書かれた方法に従ってSuperColliderやSuperDirtをインストールしつつ、Haskell関連のものだけstackでインストールしようと思います。
+⚠️行く先々でWindowsのファイアウォールの警告が出るかと思います。適当に承認しちゃってください!⚠️
+include("SuperDirt")
と書いて、「Shift + Enter」を押せば、SuperDirtのインストールが始まります。ここからはこの記事特有の手順です。
+最近のHaskell開発者は、stackというツールを使って開発環境を整えることが多いですので、冒頭の予告通りここではstackを使います。
+ちなみに、現在はHaskell Platformにもstackが添付されていますが、Haskell Platformに含まれる、GHCはstackを使うことでも簡単にインストールできるため、stackのみをインストールすれば十分です。
+なお、stack自体のインストール方法については拙作の「失敗しながら学ぶHaskell入門」のREADMEをご覧ください。
+Windowsではインストーラーをダウンロードして実行するだけで十分でしょう。
stackのインストールが終わったら、次の手順を踏んでください。
+stackでTidalCyclesのビルドをするには、C:\sr\global-project\stack.yaml
というファイルを、下記でコメントしたように書き換えてください。
# ... 省略 ...
+packages: []
+resolver: lts-12.26 # <= ここを編集
+
+extra-deps: # <= この行と、
+- hosc-0.17 # <= この行を追記
簡単に編集した内容について解説させてください。
+まず、resolver:
で始まる行ですが、これは「LTS Haskell」という、パッケージの一覧のバージョンを指定するものです。
+「LTS Haskell」は、「確実にビルドできるバージョンのパッケージをまとめた一覧」です。
+LTS Haskellのメンテナーの方々は、毎日登録された大量のパッケージをまとめてビルド・テストしてみることで、実際に登録されたバージョンのパッケージのビルドとテストが成功することを確認しています。
+なので、このLTS Haskellに登録されているバージョンのパッケージを使う限りは、私たちは安心してビルドができると言うことです。
なぜLTS Haskellのバージョンを書き換えたのかというと、それは、LTS Haskellには実際にはパッケージの一覧だけでなく、それらをビルドできるGHCのバージョンも含まれているからです。
+したがって、LTS Haskellのバージョンを指定する、ということは、そのままインストールするGHCのバージョンも指定することになります1。
+実は特に今回の場合、インストールするGHCのバージョンを指定しなければ、ビルドできない可能性が高かったのです。
+現在の最新のLTS Haskellに登録されているGHCのバージョンは「8.6.3」ですが、残念ながらこのバージョンのGHCには、Windows版のみにおいて深刻なバグがあります。
+実際にTidalCyclesをビルドする際にこのバグに遭遇するかは確かめてませんが、内容からして遭遇する確率が高そうであるという点と、遭遇するとビルドができないという点を考慮して、念のため確実にビルドできるバージョンのGHCを指定しておきました。
そして、extra-deps
という項目は、ビルドしようとしているパッケージ(今回の場合tidal
パッケージ)が依存しているパッケージが、LTS Haskellに登録されていない場合に指定するものです。
+tidalパッケージ ver. 1.0.6のパッケージ情報を確認すると、確かにhoscというパッケージに依存していると書かれていますね!
+残念ながらこのhoscパッケージは今回指定した、LTS Haskellのver. 12.26には登録されていないので、上記のとおりextra-deps
に明記しておいてください。
C:\sr\global-project\stack.yaml
の編集が終わったら、
stack build tidal
と実行しましょう。
+初回はGHCのインストールも含めて行われるので、結構時間がかかると思います。
ちなみに、stack install tidal
と実行してもいいですが、stackの仕様上、特に結果は変わりません。
+stack install
は、実行ファイルがついたパッケージをビルドしてPATH
にインストールするためのコマンドなので、tidal
のように実行ファイルがないパッケージでは意味がありません。
続いて、Atomのtidalcyclesプラグインの設定をしましょう。
+stackは使用するGHCを、前述のstack.yamlに書いたLTS Haskellのバージョンに応じて切り替える関係上、PATH
の通ったところにGHCをインストールしません。
+そのため、Atomのtidalcyclesプラグインに、stackがインストールしたGHCを認識させるには、下記のように設定を書き換える必要があります。
stack exec ghci
に書き換えてください。※公式サイトのこちらのページに対応しています。
+include("SuperDirt")
と入力した、SuperColliderのエディタに、今度はSuperDirt.start
と入力して、同じく「Shift + Enter」しましょう。.tidal
なファイルを開くか作成します。import
や、import
では賄いきれない関数の定義などが自動的に行われます。
+d1 $ sound "bd sn"
— を入力して、入力した行にカーソルを置き、「Shift + Enter」を押しましょう。
+d1 silence
と入力して同じく「Shift + Enter」を押してください。正確なエラーメッセージは申し訳なくも忘れてしまったのですが、SuperCollider上でSuperDirt.start
と入力した際、エラーになることがあります。
+この場合、SuperColliderを再起動するのを忘れている可能性がありますので、再起動してみてください。
+SuperDirtのインストールを終えた直後では、まだSuperDirtは利用できないのです。
parse error
先ほどの「Atom上でのTidalCyclesの起動」という手順で、parse error (possibly incorrect indentation or mismatched brackets)
というエラーに出遭うことがあります。
+そのままTidalCyclesの式を入力して「Shift + Enter」しても、Variable not in scope: d1 :: ControlPattern -> t
などというエラーになってしまうでしょう。
+これは、前のセクションで触れたBootTidal.hsというファイルをGHCiが読み込む際に、エラーになってしまったからです。
原因はいろいろあり得るかと思いますが、私の場合、~/.ghci
というGHCiの設定ファイルに:set +m
という行を加えていたためでした。
+まず、~/.ghci
は、GHCiが起動するときに必ず読み込まれるファイルです。
+必ず有効にしたい言語拡張や、:set +m
のようなGHCiの設定を記載しておくファイルとなっています。要するに~/.vimrc
などと似たようなものですね。
+そして:set +m
は、GHCiで複数行の入力を有効にするためのものです。
+GHCi上で:set +m
と実行すると、GHCiは入力した行を見て「あっ、この入力はまだ続きがありそうだな」と判断したとき、次の行を自動で前の行の続きとして扱うようになります。
+そして、その場合入力の終了をGHCiに伝えたい場合は、空行を入力しなければなりません。
+結果、BootTidal.hsを読み込む際に、空行が入力されないため、意図しない行が「前の行の続き」とGHCiに認識されてしまい、parse error (possibly incorrect indentation or mismatched brackets)
となってしまうようです。
仕方ないので、直すために~/.ghci
を開いて:set +m
と書いた行をコメントアウトするか削除しちゃいましょう。
+再びAtomで「Packages」 -> 「TidalCycles」 -> 「Boot TidalCycles」の順にメニューをクリックすれば、今度は該当のエラーがなく起動するかと思います😌。
このエラーは、特にすでにHaskellの開発環境を導入している方で遭遇するケースが多いかと思います。ご注意ください。
+表題の通りです。
+困ったことにSuperDirtを起動し忘れた状態でd1 $ sound "bd sn"
などの式を実行しても、特に何のエラーもなく、音も鳴りません。
+(サーバーとして起動しているべき)SuperDirtに接続し損ねたんだから、何かしらエラーが表示されてもいいはずなんですが、困ったことにウンともスンとも言いません😰。
+と、言うわけで、何のエラーもなく音も出なかった場合は、SuperDirtを起動し忘れてないか確認しましょう。
※ここから先はおまけ + 宣伝です。TidalCyclesをインストールしたいだけの方は適当に読み飛ばしてください
+ここまで、stackという、昨今のHaskellerの多くが好んで利用するツールで、TidalCyclesを利用する方法を説明しました。
+TidalCyclesの公式サイトのWikiにはこの方法は書かれてませんが、より確実なインストール方法として、覚えておいていただけると幸いです。
+すでにHaskellの開発環境をインストールしている方にも参考になるかと思います。
ところで、ここまでTidalCyclesを自分でインストールしてみて、Haskellerとしていくつか気になった点があります。
+TidalCyclesは、Haskell製の内部DSLとしては、ちょっと変わっているように感じました。
それは、TidalCyclesが「標準」として提供している関数を実行する際、tidalパッケージに含まれるモジュールをimport
するだけでなく、BootTidal.hsというファイルを読んで、追加の関数を定義する必要がある、という点です。
+大抵のHaskell製の内部DSLは、そんなことしなくてもモジュールをimport
するだけで使えるようになっています(Hspecとかlucidとかclayとかrelational-recordとか)。
+つまり本来ならばわざわざ、BootTidal.hsのような、GHCiが読み込む専用のファイルを用意しなくとも良いはずなのです。
+このBootTidal.hsはAtomのプラグインの設定で簡単に切り替えることができるものなので、もし間違ったファイルに設定してしまったら、言語の標準にあたる関数がおかしな動作をすることになりかねませんし、あまり良いやり方だとは思えません。本来なら設定に混ぜて書くべきものではないでしょう。
なぜTidalCyclesはこんな仕様になっているかというと、それにはある意味Haskellらしい制約が絡んでいると推測されます。
+Atom上でTidalCyclesを起動する、というのは、実際にはGHCiを起動して、BootTidal.hsというファイルを読み込ませる、ということなのでした(事実、Atomなどのエディターを介さなくとも、お使いのターミナルエミュレーターからghci
コマンドを起動してBootTidal.hsファイルの中身をコピペするだけで、TidalCyclesは利用できます)。
+そのBootTidal.hsの中身を見てみると、サンプルで実行したd1
という関数が、下記のように定義されていることがわかります。
-- ... 省略 ...
+import Sound.Tidal.Context
+
+-- total latency = oLatency + cFrameTimespan
+<- startTidal (superdirtTarget {oLatency = 0.1, oAddress = "127.0.0.1", oPort = 57120}) (defaultConfig {cFrameTimespan = 1/20})
+ tidal
+let p = streamReplace tidal
+
+-- ... 省略 ...
+
+let d1 = p 1
+let d2 = p 2
+let d3 = p 3
+-- ...
tidal <- startTidal
で始まる行で、TidalCyclesの初期化を行っていると思われます。
+初期化の際には、サーバーとして起動しているSuperDirtへの接続設定(この場合127.0.0.1
の57120
番ポートへ接続している)を渡しているようです。
+恐らくこのstartTidal
関数が、SuperDirtへ接続し、代入したtidal
という変数に、SuperDirtへの接続を含んでいるんでしょう。
+そして、let p = streamReplace tidal
という行で、そのtidal
をstreamReplace
関数に部分適用することで、p
がSuperDirtへの接続を参照できるようにしています。
+さらに、let d1 = p 1
などの行で、前の行で定義したp
に整数(シンセサイザーの番号だそうです)を部分適用することで、結果、d1
、d2
などの関数へ、間接的にtidal
を渡すことになります。
つまりd1
やd2
などの関数は、何らかの形で、SuperDirtへの接続情報を持っているのです。
+DSLとして、d1
やd2
などの関数に毎回接続情報を渡すのは煩雑だと考えたためでしょう。
+残念ながら、通常のHaskellがそうであるように、外部のサーバーに接続した結果取得されるものを、関数が暗黙に参照できるようにしたい場合、 — つまり、今回のようにユーザーが接続情報を明示的に渡すことなく使えるようにしたい場合 — 少なくともパッケージをimport
するだけではうまくいきません2。
+BootTidal.hsのように、SuperDirtのような外部に接続する処理を、GHCiの実行時に書かなければならないのです。
しかし、tidal <- startTidal
の行で作られるSuperDirtへの接続情報をd1
などの関数が暗黙に利用できるようにすることは、実際にはBootTidal.hsで行っているような方法を使わなくともできます。
+そうすることで、BootTidal.hsを変なファイルに切り替えてしまって、d1
などの関数の定義が間違ったものになってしまう(あるいはそもそも定義されなくなってしまう)リスクを回避できます。
+具体的には、下記のような方法が考えられます。
+申し訳なくも私はこれ以上TidalCyclesに入れ込むつもりもないので、誰かTidalCyclesを気に入った方が適当に提案するなりパッチを送るなりしてみてください(他力本願😰)。
ReaderT
を使う
+ReaderT
モナドトランスフォーマーが一番オーソドックスな方法でしょう。IO
以外のモナドのアクションでprint
することができます。ReaderT
を使ったサンプルが紹介されています。-interactive-print
というオプションに、tidal
をReaderT
経由で渡してから結果をprint
する関数を設定しましょう。d1
などをReaderT
のアクションにするだけで、それらをBootTidal.hsから消し去ることができます。return
をいちいち書かないといけなくなります。しかし、TidalCyclesの利用方法を見る限り、大きな問題にはならないだろうと思います。ImplicitParams
というGHCの言語拡張を使う
+ImplicitParams
という、もっと直接的にこれを実現する言語拡張があります。文字通り、暗黙の引数を実現するための拡張です(参考)。d1
を?tidal :: Stream => ControlPattern -> IO ()
のように型宣言しておき、?tidal
(頭に?
を付けたものが暗黙の引数となります)を暗黙の引数として参照するようにしましょう。後はGHCiの起動時に?tidal
を定義すれば、?tidal
の後にd1
などを定義する必要がなくなるので、BootTidal.hsはもっとコンパクトに済むはずです。unsafePerformIO
やTemplate Haskellなど、ちょっと危ない手段を使う
+d1
などの再利用性が下がるので、おすすめしません。TidalCyclesの技術的な側面で気になった点は以上です。
+ちょっと難しい話になってしまいましたが、これを機会に、Haskellそのものへの興味を持っていただけると幸いです。
+素晴らしいことに、TidalCyclesそのものはHaskellを知らなくてもそれなりに使えるようになっているようですが、Haskellを知った上で使えば、より簡単にトラブルシューティングができるようになりますし、TidalCyclesをより柔軟に使えるようになるでしょう。
もし、今回の記事やTidalCyclesをきっかけにHaskellを勉強してみたいと思ったら、Haskell-jp Wikiの日本語のリンク集を読んで、自分に合う入門コンテンツを見つけてみてください!
+それから、何か困ったことがあればHaskell-jpのSlack Workspaceにある、#questionsチャンネルで質問してみてください。
+登録はこちらからどうぞ!
それでは2019年もHaskellとTidalCyclesでHappy Hacking!! 🎶🎶🎶
+どのバージョンのLTS HaskellでどのバージョンのGHCがインストールされるかは、LTS Haskellを管理している「Stackage」というウェブサイトのトップページにある、「Latest LTS per GHC version」というセクションをご覧ください。↩︎
後で軽く触れる、Template Haskellという邪悪なテクニックを使わない限りは。↩︎
この記事は、Haskell Advent Calendar 2019 22日目の記事です。
+例年どおりタイプセーフプリキュア!の話をするつもりでしたが、ネタが実装できなかったのでunicode-showの話をします1。
+まぁ、こちらの方がみなさんにとっては有益でしょうし🙃
GHCiに日本語を入力したり…
+> "みんなで幸せゲットだよ!"
+ ghci"\12415\12435\12394\12391\24184\12379\12466\12483\12488\12384\12424\65281"
日本語をprint
したり…
> print "私、堪忍袋の緒が切れました!"
+ ghci"\31169\12289\22570\24525\34955\12398\32210\12364\20999\12428\12414\12375\12383\65281"
日本語をshow
したり…
> iimashita x = "今、" ++ show x ++ "って言いました!?"
+ ghci> putStrLn (iimashita "ハスケル")
+ ghci"\12495\12473\12465\12523"って言いました!? 今、
すると、日本語の大半が変な文字列に変わってしまいました😥。
+へ… 変な文字列じゃないし!エスケープシーケンスに変換しただけだから!
+これは、Haskell標準におけるshow
関数の残念な仕様です。
+show
関数に文字列を渡すと、ダブルクォートで囲った上で、ASCII範囲外の文字列や、ASCIIの非表示文字などをエスケープシーケンスに変換して返します。
+これは、show
関数をデバッグで使用した際、指定した文字列にどんな文字が含まれているか、簡単にわかるようにするための仕様です。
+文字の文字コードを表示すれば、NULL文字や制御文字、ゼロ幅文字、特殊なスペースなど、視認しにくいおかしな文字が含まれていても、一目でわかるのです。
しかしこれは日本語話者である我々にとって、少なくとも日本語の文字に関しては「余計なお世話」です。
+NULL文字やASCIIの制御文字といった本来画面に表示することがない文字列ならともかく、ASCII範囲外の文字列すべてをエスケープしてしまうのはやり過ぎでしょう。
+現代はUnicodeがあるおかげで、日本語に限らずともASCII範囲外の文字を扱うのは当たり前になりましたから。
そこで便利なのがunicode-showです。
+unicode-showのushow
関数は、show
がエスケープシーケンスに変換した日本語などの文字列を、元の文字列に戻してくれます。
+なので、新しい型クラスを定義する必要もなく、そのままShow
型クラスのインスタンスを再利用できるのです。
早速先ほどのshow
を使った例に適用してみましょう。
まずは👇のコマンドでインストールして、GHCiを起動します。
+stack build unicode-show
+stack exec ghci
+
+# あるいは、最近のcabalを使っている場合は...
+cabal v2-install --lib unicode-show
+cabal v2-repl -b unicode-show
Text.Show.Unicode
モジュールをimport
してshow
を使っている箇所をushow
に変えれば、お望みどおりの挙動になります。
> import Text.Show.Unicode
+ ghci> iimashita x = "今、" ++ ushow x ++ "って言いました!?"
+ ghci> putStrLn (iimashita "ハスケル")
+ ghci"ハスケル"って言いました!? 今、
わくわくもんですね!
+print
の例も、uprint
に変えれば🆗です。
> uprint "私、堪忍袋の緒が切れました!"
+ ghci"私、堪忍袋の緒が切れました!"
ウルトラハッピーですね!!
+さらに、次のコマンドをGHCiに入力すれば、GHCiに直接入力した日本語文字列もそのまま表示されるようになります。
+> :set -interactive-print=uprint
+ ghci> "みんなで幸せゲットだよ!"
+ ghci"みんなで幸せゲットだよ!"
カンペキ✨
+えっ、常にuprint
したいからいちいち:set -interactive-print=uprint
するのが面倒くさい?
+そんなあなたは👇を~/.ghci
に書くことけって~いでしょう。
import qualified Text.Show.Unicode
+:set -interactive-print=Text.Show.Unicode.uprint
そんなunicode-showですが、残念ながら一昨年、作者である村主崇行さんが亡くなってしまいました2。
+日本に住むHaskellerをサポートする日本Haskellユーザーグループとしては、このパッケージをメンテナンスし続けることに大きな意義があると判断し、私はこのパッケージをHaskell-jpのGitHubリポジトリーでメンテナンスすることにしました。
+以下がそのリポジトリーです。
https://github.com/haskell-jp/unicode-show
+といっても、メンテナーの名前やLICENSE
ファイルを書き換えて最新版をアップロードして以降特に何もしていなかったのですが(バグはあるけど直すのも難しそうだし、概ね使えるし)、なんと先日、Pull requestが来ました!
Do not show values eagerly by Kaiepi · Pull Request #4 · haskell-jp/unicode-show
+この修正を適用する前のunicode-showは、文字列全体を評価してからエスケープシーケンスを元に戻す、という挙動だったため、長い文字列を与えた場合や無限の長さの文字列を与えた場合に、なかなか(あるいは永遠に)結果が返ってこないという問題がありました。
+> uprint (repeat "ああああ!")
+ ghci-- 何も表示されず、Ctrl + C を押すまで処理が返らない
修正後はちゃんと遅延評価を利用することで、無限の長さの文字列でも少しずつ変換することができます。
+> uprint (repeat "ああああ!")
+ ghci"ああああ!","ああああ!", ... "ああああ!","ああInterrupted.
+ [-- Ctrl + Cを押すまで出力し続ける
今日記事にした一番の理由はこの話をするためです。
+Kaiepiさんありがとうございます!
+先ほどリリースしました!🎉
http://hackage.haskell.org/package/unicode-show-0.1.0.4
+時間がないので詳しくは省略しますが、実はpretty-simpleというパッケージを使えば、日本語をそのまま出力するのに加えて、プリティープリントできます。
+> import Text.Pretty.Simple
+ ghci> pPrint ["きーらーめーくー♪", "ほーしーの力でー♪", "あこがーれのー♪", "わーたーし描くよー♪"]
+ ghci"きーらーめーくー♪"
+ [ "ほーしーの力でー♪"
+ , "あこがーれのー♪"
+ , "わーたーし描くよー♪"
+ , ]
例ではわかりづらいですが、ちゃんと色も着けてくれます!
+それでは2020年も、Happy Haskell Hacking🎁
例年どおりですとプリキュアAdvent Calendarと同時投稿をしている予定でしたが、例年参加者が減っていたこともあり、今年はプリキュアAdvent Calendarはなくなってしまいました😞↩︎
村主崇行さんは「すごいHaskellたのしく学ぼう!」の翻訳を担当されるなど、unicode-show以外にも日本のHaskell界に多大な功績をもたらした方でした。↩︎
Haskell-jpのコンテンツの一つとしてHaskell AntennaというWebページの開発・運用をしております。
+バイナリのビルドやDockerイメージのビルドにTravisCIを、バイナリを実行してページの更新をするのにDroneCIを使っていました。 +しかし、長らく放置していてちゃんと動作しているか怪しかったので、メンテナンスをするついでに昨今はやり(要出典)のGitHub Actionsにこれらを移行することにしました。
+まずはバイナリのビルドを行うように設定します。
+Haskell AntennaのプログラムはHaskell Stackを利用しているので、stack build
が実行できれば良いです。
name: Build Application
+on:
+ pull_request: null
+ push:
+ branches:
+ - master
+jobs:
+ build:
+ name: ${{ matrix.os }}
+ runs-on: ubuntu-18.04
+ strategy:
+ fail-fast: false
+ matrix:
+ ghc: ["8.8.4"]
+ steps:
+ - uses: actions/checkout@v2
+ - name: Cache .stack
+ id: cache-stack
+ uses: actions/cache@v2
+ with:
+ path: ~/.stack
+ key: "\
+ ${{ runner.os }}-stack\
+ -${{ hashFiles('**/stack.yaml.lock') }}\
+ -${{ hashFiles('**/package.yaml') }}\
+ "
+ restore-keys: |
+
+ ${{ runner.os }}-stack- - uses: haskell/actions/setup@main
+ name: Setup Haskell
+ with:
+ ghc-version: ${{ matrix.ghc }}
+ enable-stack: true
+ stack-version: 'latest'
+ - name: Install dependencies
+ run: stack --system-ghc test --only-dependencies
+ - name: Build and Test
+ run: stack --system-ghc test --copy-bins --local-bin-path=./bin
これは、PRが作られたときやmasterがプッシュされたときに実行されることを想定しています。
+GitHub ActionsでHaskellやHaskell Stackを使うには、公式が提供しているactions/setup-haskell haskell/actions/setup を利用します。
+元々はactions/haskell-setupがありましたが、どうやらメンテナンスする人がいなくなったっぽくアーカイブされてしまいました。
+この記事を書いている時点では移行したばかりでちゃんとタグが切られていないため、mainブランチを指定しています。
+ちなみに、StackプロジェクトのGHCバージョンをhaskell/actions/setupでインストールして、stack --system-ghc
をすることでキャッシュサイズを減らすことができます。
これまた余談ですが、actions/setup-haskellの方を使っていて次のようなエラーが出る場合はactions/setup-haskellのバージョンが古いです(最新では修正済みです)。haskell/actions/setupの方を使いましょう。
+Installing ghc version 8.8.4
+Error: Unable to process command '::add-path::/opt/ghc/8.8.4/bin' successfully.
+ Error: The `add-path` command is disabled. Please upgrade to using Environment Files or opt into unsecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/
antennaプログラムはDockerイメージにしてDocker Hubに置いてあります(これもGitHub Container Registryに移行したいですね)。 +なので、masterの更新に合わせてDockerイメージをビルドしてプッシュするジョブを設定します。 +Dockerイメージのビルドとプッシュにはdocker/build-push-actionを使います。
+# さっきと同じ設定ファイルです
+name: Build Application
+on:
+ pull_request: null
+ push:
+ branches:
+ - master
+jobs:
+ build:
+ name: ${{ matrix.os }}
+ runs-on: ubuntu-18.04
+ strategy:
+ fail-fast: false
+ matrix:
+ ghc: ["8.8.4"]
+ steps:
+ ... # 割愛
+ - name: Build and Test
+ run: stack --system-ghc test --copy-bins --local-bin-path=./bin
+ # Build and Push Docker Image
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@master
+ with:
+ platforms: all
+ - name: Setup Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@master
+ with:
+ version: latest
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ builder: ${{ steps.buildx.outputs.name }}
+ tags: haskelljp/antenna:latest
+ push: ${{ github.event_name != 'pull_request' }}
+ build-args: local_bin_path=./bin
masterブランチへのプッシュのときにだけDockerイメージのプッシュをして欲しいので、push:
に github.event_name != 'pull_request'
を設定しています。
+また、Haskell Stackでビルドされたバイナリファイルは--local-bin-path=./bin
オプションで./bin
に置いてあります。
+これをDockerfileでコピーするようにしている(下記参照)ので、docker build
の引数にlocal_bin_path=./bin
というのを与える必要がありました。
FROM matsubara0507/ubuntu-for-haskell:git
+ARG local_bin_path
+RUN mkdir -p /root/.local/bin && mkdir -p /work
+ENV PATH /root/.local/bin:$PATH
+WORKDIR /work
+COPY ${local_bin_path} /root/.local/bin
このように前のstepまでの結果を利用するには context: .
を指定する必要があります(デフォルトではgit-contextというのを使うからです)。
最後に、masterの更新があったときにantennaプログラムを実行してHaskell Antennaページを更新するような設定をします。 +日毎のスケジュール実行も設定したいので、新しいワークフローを切りました。
+name: Update Antenna page
+on:
+ schedule:
+ - cron: '0 8 * * *'
+ push:
+ branches:
+ - master
+ paths-ignore:
+ - 'README.md'
+ - 'CHANGELOG.md'
+ - 'LICENSE'
+ - '.gitignore'
+jobs:
+ update:
+ name: ${{ matrix.os }}
+ runs-on: ubuntu-18.04
+ strategy:
+ fail-fast: false
+ matrix:
+ ghc: ["8.8.4"]
+ steps:
+ ... # Install dependenciesまでは一緒なので割愛
+ - name: Build
+ run: stack --system-ghc build
+
+ - uses: actions/checkout@v2
+ with:
+ ref: 'gh-pages'
+ path: 'temp'
+ - name: Exec Application
+ run: |
+
+ cp sites.yaml temp/sites.yaml
+ cp -r image/* temp/image
+ cd temp && stack exec -- antenna sites.yaml - name: Push changes
+ env:
+ COMMIT_MESSAGE: Update haskell antenna. See https://haskell.jp/antenna/ for new entries!
+ run: |
+
+ git config --local user.email "bot@example.com"
+ git config --local user.name "Bot"
+ git status
+ git add -A
+ git diff --staged --quiet || git commit -m "$COMMIT_MESSAGE"
+ git push origin gh-pages working-directory: ./temp
バイナリをビルドするところまでは一緒です。 +Haskell Antennaは同じリポジトリのgh-pagesブランチに置いて、GitHub Pagesを使って公開しています。 +なので、同じリポジトリのgh-pagesブランチをgit cloneしなおしてサブディレクトリに置き、そこでantennaプログラムを実行して、更新があった場合にのみプッシュしています。 +同じリポジトリであれば、特に設定することなくプッシュできるのがGitHub Actionsのメリットですね。
+ついでに最近のアップデートによって、ZennをHaskell Antennaに載せるサイトへ追加しました(igrep氏がしてくれました、ありがとうございます)。 +アイコンの利用規約などがわからなかったのですが、GitHubのPR上で直接聞いてみたところ、問題ないという回答をいただきました。 +突然だったのにありがとうございます。
+🎅この記事は、Haskell Advent Calendar 2020 25日目の記事です。
+🎄Happy Christmas!!🎄
今回は先日(といっても元の質問の投稿からもう何ヶ月も経ってしまいましたが…)StackOverflowに上がったこちら👇の質問に対する回答の、続きっぽい話を書こうと思います。長いし、質問の回答からスコープが大きく外れてしまうので記事にしました。
+haskell - モナド則を崩してしまう例が知りたい - スタック・オーバーフロー
+Monad
とMonoid
にある重要な繋がりを説明した後、それを応用したWriter
Monad
がどうMonoid
を使ってMonad
則を満たしているのか証明します。そして、Writer
のそうした性質を用いて簡単にMonad
則を破る例を紹介することで、読者のみなさんがMonad
則のみならずdo
記法やMonad
そのものの性質について、よりはっきりとした理解が得られることを目指します。
本記事のサンプルコードは、Haskellの構文に準拠していないものを除いて、すべてreadme-testというツールの2020年12月13日時点の開発版でテストしました。こちらのツールはまだ開発中で、今後も仕様が大きく変わる可能性がありますが、この記事のサンプルコードをテストするのに必要な機能は十分にそろっています。このreadme-test自体についてはいつか改めて共有します。
+また、テストの際に用いた環境は以下の通りです:
+Monad
とMonoid
の切っても切り離せない関係「モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?」というフレーズ(原文「A monad is a monoid in the category of endofunctors, what’s the problem?」が示すとおり、モナドとモノイド、Haskellの識別子で言うところのMonad
とMonoid
には密接な関係があります。ぶっちゃけ、このフレーズの正確な意味を私は理解していないのですが、少なくともMonad
とMonoid
には重要な共通点があることは知っています。それは、どちらも単位元と結合則がある、ということです!
具体的にMonad
とMonoid
の単位元・結合則を見てみましょう:
Monoid
の単位元: 単位元であるmempty
は、どんな値x
に<>
で足しても結果が変わらない!
<> mempty = x
+ x mempty <> x = x
Monad
の単位元: return
は>>=
の前に使っても後ろに使っても、m
やk a
の結果を変えない!
return a >>= (\a -> k a) = k a
+>>= (\a -> return a) = m m
Monoid
の結合則: x <> y <> z
の結果は、y <> z
を先に計算しようとx <> y
を先に計算しようと変わらない!
<> (y <> z) = (x <> y) <> z x
Monad
の結合則: m >>= \x -> k x >>= h
の結果は、\x -> k x >>= h
を先に計算しようと、m >>= (\x -> k x)
を先に計算しようと変わらない!
>>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h m
※Monad
の単位元・結合則の式についてはわかりやすさのために引用元から少し形を変えています。
HaskellにおけるMonad
・Monoid
とは、値がそれぞれの単位元・結合則をを満たす型です1。それ以上でも、それ以下でもありません。
それぞれの単位元・結合則を表す式は、一見して異なるものに見えるかも知れませんが、表す性質自体はよく似ています。なので、式を読んでもよく分からないという方は、上記に書いた日本語の説明をざっと眺めて覚えておいてください。特に、結合則における「~を先に計算しようと、~を先に計算しようと変わらない!」の部分がこの後とても重要になります。
+Monoid
の例ここまで読んで、Monad
はなんか聞いたことがあるけどMonoid
は初めて聞くよ、という方向けに補足すると、Monoid
とは例えば次のような型の値(と、それに対する処理)です。
Sum
型: 数値(Num型クラスのインスタンス)に対する、足し算を表すMonoid
のインスタンス
-- これから紹介する処理に必要なモジュールのimport
+import Data.Monoid
-- Sum aに対する <> は + と同等なので、
+> getSum (Sum 1 <> Sum 2 <> mempty)
+-- は、
+1 + 2 + 0
+-- と同じ。
mempty
が各Monoid
のインスタンスにおける単位元を返す、という点に注意してください。上記のとおり足し算の場合は0
です。
Product
型: 数値(Num型クラスのインスタンス)に対する、かけ算を表すMonoid
のインスタンス
-- Product aに対する <> は * と同等なので、
+> getProduct (Product 1 <> Product 2 <> mempty)
+-- は、
+1 * 2 * 1
+-- と同じ。
リスト型: リスト型の値に対する、結合 (++)
を表すMonoid
のインスタンス
-- [a] に対する <> は ++ と同等なので、
+> [1, 2] <> [3] <> mempty
+-- は、
+1, 2] ++ [3] ++ []
+ [-- と同じ
All
型: Bool
型の値に対する論理積&&
を表すMonoid
のインスタンス
> getAll (All True <> All False)
+-- は、
+True && False
+-- と同じ
-- これが何を返すかは、想像してみてください!
+mempty getAll
Any
型: Bool
型の値に対する論理和||
を表すMonoid
のインスタンス
> getAny (Any True <> Any False)
+-- は、
+True || False
+-- と同じ
-- これも何を返すかは、想像してみてください!
+mempty getAny
このように、Monoid
は他のプログラミング言語でもおなじみの、多くの二項演算を表しています。これらのインスタンスはすべて、先ほど紹介した「単位元」や「結合則」のルールを守っているので、気になった方はぜひチェックしてみてください2。
Monoid
とWriter
の切っても切り離せない関係実はそんなMonad
とMonoid
の固い絆を象徴するようなMonad
が、この世にはあります。そう、Writer
です!Writer
はMonoid
の単位元・結合則をそのまま活かすことによってMonad
の単位元・結合則を満たしたMonad
であり、Writer
がどうやってMonad
則を満たしているのか知れば、Monad
則がどうやって成立するものなのかが、すっきりクリアになることでしょう。
手始めにWriter
の定義と、Writer
がMonad
の各メソッドをどのように実装しているか見てみましょう。「モナドのすべて」におけるWriter
の紹介ページから、少しリファクタリングしつつ引用します3。
-- Writer型の定義
+newtype Writer w a = Writer { runWriter :: (a, w) }
タプルに対してnewtype
していることから分かるとおり、Writer
の実態はただのタプルです。ただのタプルがどうやってMonad
になるのでしょう?その答えがこちら👇:
-- WriterのMonad型クラスの実装
+-- 実際のところFunctor, Applicativeのインスタンス定義も必要だけどここでは省略
+instance Monoid w => Monad (Writer w) where
+return a = Writer (a, mempty)
+ Writer (a, w1) >>= f =
+ let Writer (b, w2) = f a
+ in Writer (b, w1 <> w2)
return
の定義は比較的シンプルですね。mempty
を受け取った値a
と一緒にタプルに入れて返すだけです。Monad
の単位元であるreturn
では、Monoid
の単位元であるmempty
を使うのです。
一方、>>=
はどう読めばいいでしょう?let ... in ...
の結果にあたるWriter (b, w1 <> w2)
に注目してください。
まず、b
は>>=
の右辺であるf
が返した結果です。Writer
の>>=
が返す、Writer
がラップしたタプルの一つ目の要素は、ここでf
が返した値の型と一致していなければなりません。Writer
において>>=
の型はWriter w a -> (a -> Writer w b) -> Writer w b
であり、右辺にあたるf
は(a -> Writer w b)
という型なので、>>=
全体の戻り値Writer w b
とf
の戻り値が一致している必要があることがわかりますよね?
さらに重要なのがw1 <> w2
です。ここであのMonoid
の演算子<>
が出てきました!Writer
は>>=
の中で<>
を使うMonad
なんですね!一体何と何を<>
しているのでしょう?まず、<>
の左辺であるw1
は、左辺にあたるWriter
がタプルに保持していたMonoid
型クラスのインスタンスの値です。そして右辺のw2
は、>>=
の右辺に渡した関数f
がb
と一緒に返したw2
です。
以上のことをまとめると、Writer
の>>=
は、
(a, w1)
におけるa
をf
に渡して、f
が返した(b, w2)
におけるb
を、w1
とw2
と一緒に<>
でくっつけつつ返す、という処理を行っています。Writer
は、「b
を返すついでにw1
とw2
を<>
でくっつける」と覚えてください。
Writer
は、
Monad
の単位元return
でMonoid
の単位元mempty
を使って、Monad
の結合則を満たす>>=
で、これまたMonoid
の結合則を満たす<>
を使っているのです。やっぱりWriter
はMonoid
あってのMonad
と言えますね。
do
と<>
さて、この「b
を返すついでにw1
とw2
を<>
でくっつける」というWriter
の振る舞いが象徴するように、大抵のMonad
のインスタンスにおける>>=
は、何かしら値を返すついでに、何らかの処理を行うよう実装されています。この「ついでに行われる処理」はMonad
のインスタンスをdo
記法の中で扱うと、ますます静かに身を隠すようになります。
こちらもWriter
を例に説明しましょう。まず、例示用にWriter
を作るアクションを適当に定義します。
addLogging :: Int -> Int -> Writer [String] Int
+=
+ addLogging x y Writer (x + y, ["Adding " ++ show x ++ " to " ++ show y ++ "."])
+
+multLogging :: Int -> Int -> Writer [String] Int
+=
+ multLogging x y Writer (x * y, ["Multiplying " ++ show x ++ " with " ++ show y ++ "."])
addLogging
とmultLogging
はそれぞれ、引数として受け取った整数を足し算したりかけ算したりしつつ、「足したよ」「かけたよ」という内容の文字列を一緒に返します。Writer [String] Int
における[String]
にログとして書き込んでいるようなイメージで捉えてください。
これらをdo
の中で使ってみると、よりaddLogging
やmultLogging
が「足し算やかけ算をするついでに、ログとして書き込んでいる」っぽいイメージが伝わるでしょう:
testDo :: Writer [String] Int
+= do
+ testDo <- addLogging 3 4
+ result1 <- multLogging 5 2
+ result2 addLogging result1 result2
⚠️申し訳なくもdo
記法自体の解説、つまり>>=
がどのようにdo
記法に対応するかはここには書きません。お近くのHaskell入門書をご覧ください。
👆では、3 + 4
した結果result1
と、5 * 2
した結果result2
を足す処理を行っています。それに加えて、「足したよ」「かけたよ」というログを表す文字列のリスト[String]
も一緒に返しています。do
記法が>>=
に変換されるのに従い、Writer
の>>=
が内部で<>
を使い、addLogging 3 4
・multLogging 5 2
・addLogging result1 result2
が返した文字列のリスト[String]
を結合することによって、あたかもaddLogging
やmultLogging
が「値を返しつつ、ログとして書き込む」かのような処理を実現できるのがWriter
におけるdo
記法の特徴です。
能書きはここまでにして、実際にどのような結果になるか見てみましょう:
+> runWriter testDo
+17,["Adding 3 to 4.","Multiplying 5 with 2.","Adding 7 to 10."]) (
はい、3 + 4
と5 * 2
の結果を足し算した結果17
と、addLogging 3 4
・multLogging 5 2
・addLogging result1 result2
が一緒に返していた文字列のリスト[String]
が、書いた順番どおりに結合されて返ってきました。Writer
はdo
記法の中に書いたWriter
の値(a, w)
のうち、Monoid
のインスタンスであるw
を<>
で都度結合させているということが伝わったでしょうか?
Writer
Monad
の結合則とMonoid
の結合則ここまでで、Writer
Monad
がどのように<>
を使っているのか、それによって>>=
やdo
記法がどのように振る舞っているのか、具体例を示して説明いたしました。ここからは、Writer
がMonoid
の<>
の結合則をどう利用することで、Monad
としての>>=
の結合則を満たしているのかを示しましょう。長いので「めんどい!」という方はこちらをクリックしてスキップしてください。
そのために、Monad
の結合則における>>=
を、Writer
の>>=
として展開してみます。
(0) Monad
の結合則:
>>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h m
(1) m
は>>=
の左辺なのでWriter (a, w1)
に置き換える:
※ここからは、比較しやすくするために等式=
の左辺と右辺を別々の行に書きます。
let Writer (a, w1) = m in Writer (a, w1) >>= (\x -> k x >>= h)
+=
+ let Writer (a, w1) = m in Writer (a, w1) >>= (\x -> k x) >>= h
(2) 一つ目の>>=
をWriter
における>>=
の定義で置き換える:
let Writer (a, w1) = m
+Writer (b, w2) = (\x -> k x >>= h) a
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m in Writer (a, w1) >>= (\x -> k x) >>= h
(3) 等式=
の右辺における一つ目の>>=
も同様に変換する:
let Writer (a, w1) = m
+Writer (b, w2) = (\x -> k x >>= h) a
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m
+Writer (b, w2) = (\x -> k x) a
+ in Writer (b, w1 <> w2) >>= h
(4) 無名関数である(\x -> k x >>= h)
と(\x -> k x)
に、a
を適用する:
let Writer (a, w1) = m
+Writer (b, w2) = k a >>= h
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m
+Writer (b, w2) = k a
+ in Writer (b, w1 <> w2) >>= h
(5) 等式=
の左辺における二つ目の>>=
をWriter
における>>=
の定義で置き換える:
let Writer (a, w1) = m
+Writer (b, w2) =
+ let Writer (c, w3) = k a
+ Writer (d, w4) = h c
+ in Writer (d, w3 <> w4)
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m
+Writer (b, w2) = k a
+ in Writer (b, w1 <> w2)) >>= h
(6) 等式=
の右辺における二つ目の>>=
も同様に変換する:
let Writer (a, w1) = m
+Writer (b, w2) =
+ let Writer (c, w3) = k a
+ Writer (d, w4) = h c
+ in Writer (d, w3 <> w4)
+ in Writer (b, w1 <> w2)
+ =
+ let Writer (a, w1) = m
+Writer (b, w2) = k a
+ in let Writer (c, w3) = Writer (b, w1 <> w2)
+ Writer (d, w4) = h c
+ in Writer (d, w3 <> w4)
(7) Writer
は、Writer
と(a, w)
を切り替えるだけで実質何もしていないので削除する:
let (a, w1) = m
+=
+ (b, w2) let (c, w3) = k a
+ = h c
+ (d, w4) in (d, w3 <> w4)
+ in (b, w1 <> w2)
+ =
+ let (a, w1) = m
+= k a
+ (b, w2) in let (c, w3) = (b, w1 <> w2)
+ = h c
+ (d, w4) in (d, w3 <> w4)
(7.5) (7)の等式をよく見ると、=
の左辺においては(b, w2)
と(d, w3 <> w4)
が、=
の右辺においては(c, w3)
と(b, w1 <> w2)
が等しい。
let (a, w1) = m
+= -- ここの(b, w2)は、
+ (b, w2) let (c, w3) = k a
+ = h c
+ (d, w4) in (d, w3 <> w4) -- ここの(d, w3 <> w4)を代入したもの!
+ in (b, w1 <> w2)
+ =
+ let (a, w1) = m
+= k a
+ (b, w2) in let (c, w3) = (b, w1 <> w2) -- ここで代入している!
+ = h c
+ (d, w4) in (d, w3 <> w4)
(8) (7.5)から、=
の左辺ではb = d
でw2 = w3 <> w4
、=
の右辺ではc = d
でw3 = w1 <> w2
であることがわかる。なのでそれぞれ置き換える:
let (a, w1) = m
+= k a
+ (c, w3) = h c
+ (d, w4) in (d, w1 <> (w3 <> w4))
+ =
+ let (a, w1) = m
+= k a
+ (b, w2) = h b
+ (d, w4) in (d, (w1 <> w2) <> w4)
(9) a
~d
・w1
~w4
の変数名を、登場した順番に振り直す:
let (a, w1) = m
+= k a
+ (b, w2) = h b
+ (c, w3) in (c, w1 <> (w2 <> w3))
+ =
+ let (a, w1) = m
+= k a
+ (b, w2) = h b
+ (c, w3) in (c, (w1 <> w2) <> w3)
等式=
の左辺と右辺がそっくりな式になりましたね!
ここで、Monoid
の結合則を思い出してみましょう:
<> (y <> z) = (x <> y) <> z x
そう、x <> y <> z
などと書いて3つのMonoid
型クラスのインスタンスの値を<>
でくっつけるときは、カッコで囲って(y <> z)
を先に計算しようと、(x <> y)
を先に計算しようと、結果が変わらない、というものでした!
それを踏まえて、(9)の等式=
の両辺をよく見比べてみてください。異なっているのはw1 <> (w2 <> w3)
と(w1 <> w2) <> w3)
の箇所だけですね!つまり、Writer
Monad
における>>=
の結合則は、w1 <> (w2 <> w3)
と(w1 <> w2) <> w3)
が等しいから、すなわちMonoid
における<>
の結合則が成り立つからこそ成立するのです。これがまさしく「Monoid
とWriter
の切っても切り離せない関係」なのです!
それではいよいよ、「Monoid
とWriter
の切っても切り離せない関係」を利用して、Monad
則を破ってみましょう💣
<>
とMonoid
の結合則前述のとおり、Writer
における>>=
が結合則を満たすのは、Writer
がラップしているMonoid
な値の<>
が結合則を満たしてこそ、なのでした。これは言い換えれば、その、ラップしているMonoid
な値の<>
が結合則を破れば、自然にWriter
の>>=
も結合則を破るはずです。この方法は、結合則を満たさない>>=
っぽい処理をゼロから探すより遥かに簡単です。>>=
のようなm a -> (a -> m b) -> m b
というややこしい型の関数よりも、<>
のようなa -> a -> a
という型の関数の方がずっと身近ですしね!
Monoid
の<>
のようなa -> a -> a
という型の関数で、結合則を満たさない処理 — といえば、引き算-
や割り算/
を思い浮かべる方が多いのではないでしょうか。と、いうわけでMonoid
の例で紹介したSum
やProduct
のように、数値に対する引き算を表すnewtype
、Difference
を定義してみましょう:
newtype Difference a = Difference { getDifference :: a }
それから、Difference
を(実際には間違いですが)Monoid
のインスタンスにします。最近のGHCでは、Monoid
のインスタンスを定義する前にSemigroup
のインスタンスにする必要があるのでご注意ください。説明しやすさのために敢えてこれまで触れてきませんでしたが、これまで何度も使った<>
は実際のところMonoid
の関数ではなくSemigroup
の関数なんですね。Monoid
は「<>
で(結合則を備えた)二項演算ができるだけでなく、mempty
という単位元もある」という性質の型クラスなので、「単に『<>
で(結合則を備えた)二項演算ができる』だけの型クラスも欲しい!」というニーズから、Monoid
の<>
はSemigroup
の関数となり、Monoid
はSemigroup
のサブクラスという関係に変わったのでした。
何はともあれ、Difference
をSemigroup
のインスタンスにしましょう:
instance Num a => Semigroup (Difference a) where
+Difference a <> Difference b = Difference (a - b)
はい、単に両辺を-
で引き算するだけですね。
今度こそDifference
をMonoid
のインスタンスにします。本記事ではmempty
を直接使うことはないので何でもいいはずですが、とりあえずSum
と同様に0
ということにしておきます:
instance Num a => Monoid (Difference a) where
+mempty = Difference 0
😈これで<>
が結合則を満たさないおかしなMonoid
のインスタンス、Difference
ができました!早速試して結合則を破っていることを確認してみましょう:
-- こちらは 1 - (2 - 3) と同じ
+> getDifference $ Difference 1 <> (Difference 2 <> Difference 3)
+2
+
+-- こちらは (1 - 2) - 3 と同じなので...
+> getDifference $ (Difference 1 <> Difference 2) <> Difference 3
+-4 -- <- 当然 1 - (2 - 3) とは異なる結果に!
バッチリ破れてますね!このように<>
における結合則は、引き算などおなじみの演算で、簡単に破ることができます💪
>>=
とMonad
の結合則<>
における結合則を破ることができたと言うことは、Writer
の>>=
による結合則も、もはや破れたも同然です。先ほど定義したDifference
型を使えば、>>=
は途端に結合則を満たさなくなるでしょう。
例を示す前に、Writer
を使う際しばしば用いられる、ユーティリティー関数を定義しておきます。実践でWriter
を使いたくなったときにも大変便利なので、是非覚えておいてください:
tell :: Monoid w => w -> Writer w ()
+= Writer ((), w) tell w
このtell
関数は、受け取ったMonoid
な値をそのまま「ログとして書き込む」関数です。結果として返す値はただのユニット()
なので、気にする必要がありません。tell
のみを使ってWriter
を組み立てれば、「ログとして書き込む」値のみに集中することができます。これから紹介する例でもやはり関心があるのは「ログとして書き込む」値だけなので、ここでtell
を定義しました。
それではtell
を使って、Writer
の>>=
における結合則も破ってみましょう:
-- こちらは Difference 1 <> (Difference 2 <> Difference 3) と同じ
+> getDifference . snd . runWriter $ tell (Difference 1) >>= (\_ -> tell (Difference 2) >>= (\_ -> tell (Difference 3)))
+2
+
+-- こちらは (Difference 1 <> Difference 2) <> Difference 3 と同じなので...
+> getDifference . snd . runWriter $ (tell (Difference 1) >>= (\_ -> tell (Difference 2))) >>= (\_ -> tell (Difference 3))
+-4 -- <- 当然 1 - (2 - 3) とは異なる結果に!
予想どおり一つ目のWriter
と二つ目のWriter
とで異なる結果となりました。1 - (2 - 3)
と(1 - 2) - 3
をWriter
を使って遠回しに言い換えているだけなので、当然と言えば当然です。
しかしtell (Difference 1) >>= (\_ -> tell (Difference 2) >>= \_ -> tell (Difference 3))
などのWriter
型の式がMonad
の結合則m >>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h
にどう対応するのか、ちょっと分かりづらいですかね?(式も長いし)一つずつ注釈を加えます:
-- こちらは m >>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h の前半、
+-- m >>= (\x -> k x >>= h) に相当する
+> tell (Difference 1) >>= (\_ -> tell (Difference 2) >>= (\_ -> tell (Difference 3)))
+-- ^^^^^^^^^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-- m x k h
+--
+
+-- こちらは m >>= (\x -> k x >>= h) = (m >>= (\x -> k x)) >>= h の後半、
+-- (m >>= (\x -> k x)) >>= h に相当する
+> (tell (Difference 1) >>= (\_ -> tell (Difference 2))) >>= (\_ -> tell (Difference 3))
+-- ^^^^^^^^^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-- m x k h
ラムダ式の引数x
は実際には使われていない点に注意してください。これでもconst
を使って\x -> const (tell (Difference 2)) x
と書き換えれば、const (tell (Difference 2))
がk
に厳密に対応するので、上記の二組の式は>>=
の結合則を破るペアだと言えます。
do
記法とMonad
の結合則前の節では、Monoid
の結合則を守っていない値をラップしているWriter
を作ることで、>>=
の結合則を破る例を簡単に作り出せることを紹介しました。ここでは本記事の最後として、>>=
の結合則を破った結果、do
記法がいかに直感に反する挙動となるか紹介して、>>=
の結合則を守ることが私たちにどのようなメリットをもたらすのか解説します。
例として、先ほど>>=
の結合則を破るのに使った1 - 2 - 3
を再利用しましょう。Difference
をラップしたWriter
で1 - 2 - 3
を計算させると、次のような式になります:
Difference 1) >>= (\_ -> tell (Difference 2)) >>= (\_ -> tell (Difference 3)) tell (
これをdo
記法に変換すると、次のようになります:
do
+Difference 1)
+ tell (Difference 2)
+ tell (Difference 3) tell (
do
記法における各行の間に>>=
が隠れたことで、すっきりしましたね!
この状態から、do
記法を使って1 - (2 - 3)
と(1 - 2) - 3
を表すWriter
の式にするには、次のように書き換えます:
-- こちらが 1 - (2 - 3) を表す
+=
+ do_1minus'2minus3' do
+ Difference 1)
+ tell (do
+ Difference 2)
+ tell (Difference 3)
+ tell (
+-- こちらが (1 - 2) - 3 を表す
+=
+ do_'1minus2'minus3 do
+ do
+ Difference 1)
+ tell (Difference 2)
+ tell (Difference 3) tell (
コメントに書いたとおり、do_1minus'2minus3'
が1 - (2 - 3)
、do_'1minus2'minus3
が(1 - 2) - 3
と同等なWriter
です。Haskellはシングルクォートを変数の名前に含めることができるので、シングルクォートでカッコを表すことにしました(まさかこんなところで役に立つとはね!)。
上記の二つの式では、カッコ()
で囲う代わりにもう一つのdo
記法に収めることで、do
記法における各行を実行する順番をいじっています。
本当にこれで1 - (2 - 3)
や(1 - 2) - 3
と同等な式になっているのでしょうか?試しにrunWriter
して結果を確かめてみましょう:
-- こちらが 1 - (2 - 3) を表す
+> getDifference . snd $ runWriter do_1minus'2minus3'
+2
+
+-- こちらが (1 - 2) - 3 を表す
+> getDifference . snd $ runWriter do_'1minus2'minus3
+-4
バッチリ👌想定どおり、do_1minus'2minus3'
が1 - (2 - 3) = 2
を計算し、do_'1minus2'minus3
が(1 - 2) - 3 = -4
を計算していますね!
さてこれまでで、Writer
Monad
はMonoid
の結合則を利用することで>>=
の結合則を満たしていることを示し、ラップしているMonoid
な値が結合則を満たしていなければ、必然的にWriter
も結合則を破ってしまうことを、>>=
やdo
記法を使って具体的に示しました。それでは今挙げた、do
記法で結合則を破った例は、一体何を示唆しているのでしょうか?普通にHaskellでコードを書いていて、前述のような書き換え、すなわち、
do
+Difference 1)
+ tell (do
+ Difference 2)
+ tell (Difference 3) tell (
から、
+do
+do
+ Difference 1)
+ tell (Difference 2)
+ tell (Difference 3) tell (
への書き換え(あるいはその逆)は、一見するとそんな機会ないように思えます。しかしこれが、do
記法をカッコ代わりに使うという変な方法ではなく、次のように変数に代入することで切り出していた場合、いかがでしょうか?
= tell (Difference 1)
+ someSingleAction
+= do
+ someSequence Difference 2)
+ tell (Difference 3)
+ tell (
+= do
+ someCompositeAction
+ someSingleAction someSequence
上記👆のような三つのWriter
の値を、下記👇の三つの値にリファクタリングする場合です。
= do
+ refactoredSequence Difference 1)
+ tell (Difference 2)
+ tell (
+= tell (Difference 3)
+ splitOutSingleAction
+= do
+ refactoredCompositeAction
+ refactoredSequence splitOutSingleAction
あるいは、たった3行しかありませんし、一つの値に統合する方がいいかも知れません:
+= do
+ flattenedAction Difference 1)
+ tell (Difference 2)
+ tell (Difference 3) tell (
これらの書き換えは、いずれもdo
記法が内部で使っている>>=
の結合則を前提とすれば、可能であってしかるべきです。do
記法は、適当にMonad
のインスタンスの値(「アクション」などとも呼ばれます)を上から下まで列挙すれば、自動で>>=
を使ってつなげてくれる、というものです。なので、適当に並べたアクションがどういう形に結合されるのか気にする必要があるのでは、安心して使えません。一方、上記の3組の式は、Writer Difference
、すなわち引き算を表す「偽Monoid
」をラップしているが故に、>>=
の結合則を満たしておりません。結果、do
記法に変えたときに並べたアクションをどこで切り出すかで、結果が変わってしまいます。これでは安心して列挙できません!
以上です。これまでで、Monad
則のうち結合則がなぜ重要なのか、結合則を実際に破ってみることを通じて説明しました。Monad
と同様に結合則を持ったMonoid
は、Monad
以上にインスタンスを見つけるのが簡単で、なおかつ、例えば引き算のように「二項演算だけど結合則を満たしていない」処理を見つけるのが簡単です。本記事ではMonoid
のそうした性質と、Monoid
の性質でもってMonad
則を満たしているWriter
Monad
に注目することで、簡単にMonad
則を破る例を提示することができました。それから、Monad
の結合則を実際に破った例を使って、Monad
の結合則がdo
記法を自然に書けるようにするために必要であることを示しました。これらの実例から主張したいことを一般化すると、次のとおりです:
do
記法の各行の間で、値を返すついでに何かを行うのがMonad
のインスタンスdo
記法の各行の間で、値を返すついでに行っている処理が結合則を満たす型が、Monad
則を満たすと言えるMonad
則を守らない型をdo
記法で使うと、do
記法の結合を気にして書かなければならなくなるそれでは、2021年も🎁Happy Haskell Hacking with Monad🎁!
+一応、Monad
についてはそのスーパークラスであるApplicative
の則、Functor
の則がありますが、Monad
則を満たしていればそれらは自動的に満たせるので、ここでは省略します。↩︎
残念ながら実際のところ、Float
型・Double
型などの浮動小数点数に対するSum
やProduct
は結合則を満たさない場合があります。これは他の多くのプログラミング言語にもある、浮動小数点数の悩ましい問題です。詳しくは「情報落ち」で検索してみてください。↩︎
ここでの定義は、実際に使われているtransformersパッケージのWriter
の定義とは大きく異なっているのでご注意ください。実際のWriter
はパフォーマンス上の都合やMonad Transformerとの兼ね合いで、幾分工夫された定義となっています。↩︎
この記事では、Haskellを業務でカジュアルに使う観点やヒントについて、簡単に紹介します。
+Haskellを業務で使える局面は、以下のようにいくつか考えられます。
+つまり、プロダクトの開発用言語としてHaskellを用いない業務形態においても、上記2や3のケースとして、Haskellを使用できます。すなわち、Haskellは幅広い局面でカジュアルに、つまり気軽に手軽に使用できます。
+本記事では、特に、上記の2と3について、いくつかの観点やヒントや例を紹介します。
+なお、上記は、Haskellを用いる場合には限りません。Python, Perl, Ruby, Rust, Scala, OCaml, Clojure, Go, Elixir, … といった、様々なプログラミング言語に置き換えて本記事を解釈してもらって構いません。
+Haskell(を含むプログラミング言語)は、開発などの日常業務において、「作業」の支援・加速用に使うことが出来ます。
+つまり、電卓やExcelなどのように、Haskellを日常ツールの一つとして使えます。
+特に、直近の業務作業を加速するために、書き捨てのツールを高品質で素早く欲しい場合や、ちょっとした対話ツールを欲しい場合などにも、Haskellを便利に活用できます。
+例えば具体的には、以下の場合にHaskellを便利に使えます。
+以下、それぞれについて簡単に紹介します。
+例えば、解析事案が発生し、至急10分程度でテストデータを複数用意したい、というような場合に、Haskellでデータを生成させることは有効です。
+Haskellは、関数合成や部分適用や高階関数や多相関数などの言語的な特徴により、小さな関数を組み合わせて、より大きな関数として作り上げることが容易です。
+対話環境(REPL)であるGHCiを用いて、それら小さな関数を素早く高品質に確認した上で、徐々に大きな関数として組み合わせることにより、高品質な結果を素早く得ることがでできます。
+特にバイナリデータや複雑なデータを、一刻も早く高品質に生成することが重要な局面で、Haskellは威力を発揮します。
+日常業務において、各種ログなどのデータを解析したい局面は頻繁に有ります。 +単純なデータであれば、grepコマンドやPerlなどの正規表現を用いて手早く仕事を済ませることも出来ます。
+しかし、データの構造が複雑であったり再帰的な構造である場合には、正規表現をデバッグするよりも、Haskellで思い切ってパーサーを書いてしまう方が手早く済ませられることがあります。
+Haskellでは、関数の組み立てが容易であることやdo記法といった言語的な特徴を活かし、簡潔にパーサーを記述することができます。
+言語的な特徴を活かした便利なパーサーコンビネータ関連のライブラリ(Parsec
やMegaparsec
やreplace-attoparsec
など)が豊富に存在します。
一度パーサーの骨格を用意してしまえば、流用は容易であるため、強力な日常ツールとしてHaskellを便利に使用できます。
+例えばモジュールの構造に対応したデータのように、データが再帰的・階層的に表現されている場合は多くあります。
+Haskellは、代数的データ型を用いて再帰的なデータ構造を簡潔に表現できます。また、簡潔なパターンマッチの記法と再帰的な関数により、これらの処理を容易に記述できる傾向にあります。
+もちろん、この再帰的なデータ構造も、コンパイル時の静的な型チェックの対象となるため、多くの不用意なミスを事前に抽出できます。
+素早く、非常に高品質にデータ処理を行うことが重要な局面で、Haskellは有効に機能します。
+日常業務において、なんらかの変換テーブルや、計算式、定数値などの値を、散発的に直ちに得たい局面があります。 +その都度、電卓で計算したり、Excelなどの計算フォームを用意することで、手軽に業務を済ませられる場合もあります。
+しかし、繰り返し必要となる計算式や、ある程度複雑な計算であれば、これらの計算式などを、Haskellの関数群として定義しておき、対話環境GHCiから用いることで、使い勝手良く素早く値を得ることができます。
+数値や対話操作などを補助する便利なライブラリ(Numeric
やData.Bits
やData.GHex
)や言語拡張(BinaryLiterals
やNumericUnderscores
)などが豊富に存在します。
Haskellにおける関数の組み立てが容易な特徴は、対話環境における対話的な操作との相性が良いため、試行錯誤的な計算作業にも有用です。
+他にも、定型的なファイル処理やCLIコマンドやDSLの構築などを、Haskellを用いて便利に実現出来ます。 +手元に各種雛形を蓄積していると、作業の素早さと正確さが求められる場合に、有益でしょう。
+もちろん、これらはHaskellに限らず、多くのプログラミング言語にも言えます。
+Haskellは、型システムに守られながら、関数を容易に組み立てられる特徴を持ちます。また、代数的データ型とパターンマッチの特徴により、直感的・シンプルで高品質なデータ表現・処理が可能です。さらに、GHCiを用いる対話操作により、日常作業を高品質かつ手早く行えます。
+Haskellは、(型システムの高度な機能などを使わない)基本的な機能のみにおいても、日常業務において有効に活用できるツールの一つです。
+Haskell(を含むプログラミング言語)は、開発などの日常業務において、「思考」の支援・加速用にも使うことが出来ます。
+つまり、紙と鉛筆などのように、Haskellを思考ツールの一つとして使えます。
+特に、試行錯誤的な思考フェーズや、探索フェーズにおいて、思考を整理・加速する場合などに、Haskellは便利です。
+例えば具体的には、以下の場合にHaskellを便利に使えます。
+以下、それぞれについて簡単に紹介します。
+ハードウェアやソフトウェア開発過程などでは、例えば、自然言語と図表や式の組み合わせで表現された仕様書を理解する事が必要な局面が多くあります。
+設計の上流工程で思考を広く深く及ばせておくことにより、仕様に対する思わぬ考え漏れや勘違いを防ぐことは、開発全体の質や開発速度を上げる観点で非常に有効です。
+Haskellは、代数的データ型やパターンマッチを簡潔に記述できる言語的な特徴を持つため、仕様を簡潔に表現することに向いています。さらに、対話環境GHCiを用いて、自分の考えを試行錯誤的に確認できます。
+自然言語等の仕様を、プログラミング言語を用いて表現・写経する過程は、単純ですが、対象への理解を深める上で、意外に大きな投資対効果があります。Haskellは、このような場合に強力なツールとなります。
+設計の初期段階において、自分の考えミスを抽出するために、設計の中核部分を簡単なモデルで表現して確認することは、開発全体の質や開発速度を上げる観点で非常に有効です。
+前節の仕様理解の場合と同様に、設計の中核モデルを簡潔に記述する目的で、Haskellを用いることが出来る場合があります。
+Haskellの代数的データ型とパターンマッチは、モデルの簡潔表現にもフィットする場合が多く、自分の考えを手早く確認することに有効に使用できます。
+さらに、Haskellで記述したモデルを、QuickCheck
ライブラリなどによるランダムテストパターンを用いて簡易検査することにより、値の範囲や特性に対する考え不足を、容赦なく効率的に抽出できます。
設計の初期段階において、モデルのパラメータなどについての設計空間を、試行錯誤しながら探索したい局面があります。
+前節のモデル理解の場合と同様に、設計空間を探索する目的で、Haskellを用いることが出来る場合があります。
+Haskellの代数的データ型とパターンマッチを用いてモデルを簡潔に記述できれば、系の大きさなどの多くのパラメータを振りながら、最適な設計値を探索することに活用できます。
+思考フェーズでは、記述したプログラムの実行速度よりも、思考内容をコードで表現する速さや、試行錯誤的にコードの内容を確認・変更する速さの方が重要なことが有ります。
+各々の人の思考特性によりますが、Haskellの代数的データ型とパターンマッチなどの言語的な特徴は、実行可能仕様書・実行可能思考表現として、思考を整理することに向いています。
+以下のように、Haskellを用いて、簡潔に、素早く、手軽に、思考作業を支援・加速できます。
+便利ですね。
+この記事では、Haskellを業務でカジュアルに使う観点やヒントについて紹介しました。 +「作業」や「思考」が必要な、よりたくさんの局面でHaskellを使用できます。
+関数合成、部分適用、高階関数、多相関数などの言語的な特徴は、関数をボトムアップや対話的に、素早くかつ高品質に組み上げるのに便利です。代数的データ型などの言語的な特徴は、ある種の思考パターン(選択、非一様、入れ子など)をストレートに表現するのに便利です。対話環境GHCiは、試行錯誤的に作業や思考を進めるのに便利です。
+Haskellに限らず、自分の思考特性にあったプログラミング言語を、業務を加速する日常的なツールとして備えておくことは有用です。
+しかし、そもそもプログラミング言語の可能性・適用範囲は非常に広いものです。その適用範囲を、「業務」に狭めてしまう必要もありません。
+プログラミング言語は、業務のみに限らず、日々の「思考」の支援・加速に広く使用できるものです。
+以上、 Enjoy programming!
+Haskell プログラミングにおいて,データ型は非常に重要な役割を持つ.データ型は,扱うデータをプログラミング上で安全かつ容易に加工するために用いられ,またデータに対してどのような操作ができるのかを規定する.
+Haskell には,データ型を新たに定義する方法が3つある.
+type
キーワードによって定義する方法で,これにより定義されたデータ型は型シノニムと呼ばれる.data
キーワードによって定義する方法で,これにより定義されたデータ型は代数的データ型と呼ばれる.newtype
キーワードによってある型を元に新たな型を作る方法だ.今回は,それぞれどういう使い方をするのか,どういう違いがあるのかについて見ていきたいと思う.
+例えば,あなたは Web サイトを運営していて,一部年齢制限が必要なため,人の年齢が 20 歳以上かを判定する関数を書かなければいけないとする.年齢は整数だが,入力は必須でないため入力してない人もいる.その場合は,20 歳以上でないと判定する.この関数は,
+isAdult :: Maybe Int -> Bool
+= case m of
+ isAdult m Nothing -> False
+ Just x -> x >= 20
と書ける.ただ,この定義はどこか味気ない.isAdult
が受け取るデータは,年齢を表していて,整数か未詳かの状態を持つので,Maybe Int
はデータを正確に捉えられている.しかし,Maybe Int
に適合するデータは他に無数にあるため,isAdult
が受け取るデータが年齢を表すのか知能指数を表すのか,はたまた今までお酒を飲んだことのある回数なのかは推測しないと分からない.年齢を表すデータ型を新たに定義して,それを受け取るようにすればもっとプログラムがクールになるだろう.
Haskell で新しくデータ型を定義する最も簡単な方法は,type
キーワードを使って型シノニム (type synonym) を定義する方法だ.シノニムとは,別名という意味で,型シノニムは文字通り,ある型の別名を表す.今回は次のように使える:
type Age = Maybe Int
+
+isAdult :: Age -> Bool
+= case age of
+ isAdult age Nothing -> False
+ Just x -> x >= 20
これで関数 isAdult
は,先ほどと比べてとても明確になった.Age
は Maybe Int
を元に作られた型シノニムで,つまり Age
は Maybe Int
の別名になっている.単なる別名なので,isAdult
は Maybe Int -> Bool
型の関数だと思って使うこともできる.GHCi で試してみよう:
>>> (isAdult :: Maybe Int -> Bool) (Just 22 :: Age)
+True
Maybe Int
を Age
だと思うこともできるしその逆もできる.型シノニムと元となった型は自在に取り替え可能だ.型シノニムはとても手軽なので,Haskell の標準ライブラリでも使われている.例えば,次のようなデータ型が型シノニムで定義されている:
type String = [Char]
+type FilePath = String
文字列は文字のリストと見做せる.そこから文字列によるデータ型 String
は,単に文字のリスト型の型シノニムで定義されている.文字列に対してリストの関数を自由に適用できるのは,このためだ.ファイルのパスによるデータ型 FilePath
は String
の型シノニムで定義されている.なので,文字列の関数を自由に適用できる.
Haskell の型シノニムは,これだけに止まらずもっと強力な機能も持っている.例えば,型シノニムは型コンストラクタ,すなわち型を受け取って新たな型を作るコンストラクタに対しても作れる:
+type Option = Maybe
この型シノニムを使うと,Maybe Int
と書く代わりに Option Int
と書くことも可能だ.部分適用された型コンストラクタに対する型シノニムも書ける:
type Failable = Either String
この型シノニムを使うと,Either String ()
と書く代わりに Failable ()
と書くことができる.
さらに型シノニムは,パラメータを持つことができる:
+type List a = [a]
この型シノニムを使うと,[Int]
は List Int
と書ける.ただし,型シノニムはあくまで別名なので,全てのパラメータを適用した状態でしか書けないことに注意する必要がある.例えば,次のプログラムはコンパイルエラーになる:
type Apply f a = f a
+type ApplyMaybe = Apply Maybe
Apply
は2つのパラメータをとるが,ApplyMaybe
は Apply
に1つのパラメータしか渡していない.この場合,Apply Maybe
という型がどういう型の別名になるか Haskell は分からないため,この型を拒否する.このプログラムを修正するには,
type Apply f a = f a
+type ApplyMaybe a = Apply Maybe a
というように,Apply
に全ての引数を渡してやる必要がある.こうすることで,Haskell は Apply
の定義から Apply Maybe a
が Maybe a
の別名であると認識できるようになる 1.
型シノニムは,他にも幾つか用途上で制限がある.1つ目は再帰的な型シノニムが作れないという制限だ.例えば,
+type InfiniteList a = (a, InfiniteList a)
という定義は Haskell では却下される.相互再帰的な定義も許容されていない:
+type Rec1 = [Rec2]
+type Rec2 = [Rec1]
Rec1
の型を具体的に求めようとすると,[Rec2]
の型になる.Rec2
はやっぱり型シノニムで,[Rec1]
の別名なので,この型はさらに [[Rec1]]
という型になる.このようにして具体的な型を求めようとしても永遠に型シノニムがどこかしらに入り込むことになってしまい,型シノニムが現れない型を求めることはできない.Haskell ではそのようなことがないように,そのような定義を排除している 2.
もう1つの制約は,型シノニムを型クラスのインスタンスとして使えないというものだ.例えば,次のようなことはできない:
+type I = Int
+
+class C a
+instance C I
代わりに,
+class C a
+instance C Int
というように型シノニムを使わず書く必要がある.これは型シノニムを使って書けない唯一の例外だ.ただ,この制限は本質的なものではなく,Haskell 標準で型シノニムに対する混乱を避けるための制限になっている.もし,型シノニムに対してインスタンスを書けるようにしても,型シノニムは単なる別名なので,それは元となった型に対してインスタンスを定義してることと同じになる.このため,
+f :: C a => a -> a
+= x f x
という関数は,type Age = Int
による型シノニム Age
に対して C
のインスタンスが定義されていた場合,a
が Age
の場合も Int
の場合も許容される.これは,プログラマが意図していない動作かもしれない.つまり,年齢のデータだけにインスタンスを定義したつもりが,整数データ全般に対していつのまにかインスタンスを定義してしまったことになるからだ 3.
これらの制限はあるものの,型シノニムはデータ型を定義する上でとても強力で,しかも簡単に使用できる機能だ.
+さて,型シノニムでデータ型を定義する場合には幾つかの制限があった.では,この制限を超えたデータ型を定義する方法はないのだろうか? そのような場合には代数的データ型 (algebraic datatype) を使うことができる.
+代数的データ型は,複数の型の値を統合して1つの型の値として扱うデータ型の積と,複数の型の表現範囲を合わせて1つの型として扱うデータ型の和を組み合わせることで構成されている.そして,このデータ型の定義は,型シノニムと異なり完全に新しい型を作り出す.実際の例を見てみよう.
+あなたは積木パズルのパーツそれぞれの面積を計算する関数を,書かなければいけない.積木パズルのパーツはそれぞれ,長方形,真円,三角形から構成されている.まずはこのパーツを Haskell のデータ型に落とし込む必要がある.それぞれのパーツにおいて,
+によって特徴付けられている.では,これを代数的データ型に落とし込んでみよう:
+data PuzzleElement
+= Rect
+ Double
+ -- ^ 縦の長さ
+ Double
+ -- ^ 横の長さ
+ | Circle
+ Double
+ -- ^ 半径
+ | Triangle
+ -- ^ 三つの辺の長さを与える
+ Double Double Double
この定義は,PuzzleElement
という新しい型を作り,3つの値コンストラクタを作る.それぞれ
Rect :: Double -> Double -> PuzzleElement
Circle :: Double -> PuzzleElement
Triangle :: Double -> Double -> Double -> PuzzleElement
という型を持つ.Rect
は Double
型の値を2つ受け取り,その2つの値を PuzzleElement
型の1つの値として統合する.つまり,Double
型2つの積を作る.Circle
や Triangle
も同様だ.そして,PuzzleElement
型は3種類の積の値のいずれかを表し,すなわちこれら3種類の積の和を表す.このように,積和によって新しいデータ型を定義できるのが data
宣言であり,それによって定義されるのが代数的データ型になる.
代数的データ型の値から統合した値を取り出したい時は,case
文を使ったパターンマッチを行う:
areaMeasure :: PuzzleElement -> Double
+= case x of
+ areaMeasure x Rect w h -> w * h
+ Circle r -> r * r * pi
+ Triangle s1 s2 s3 ->
+ let s = (s1 + s2 + s3) / 2
+ in sqrt $ s * (s - s1) * (s - s2) * (s - s3)
areaMeasure
によってパズルのピースの面積を求めることができるようになった.
前に紹介した型シノニムは,ある型に対してその別名を与えるだけだった.それに比べ,代数的データ型では新しいデータ型を作り,その型の値を作る値コンストラクタを定義する.そして,型シノニムと大きく異なる点は,型システム上からは新たに定義された型しか分からず,実際にそのデータ型がどういう型から構成されるか分からない点にある.PuzzleElement
型の値は,もしかしたら Double
型の2つの値から Rect
コンストラクタを介して作られているかもしれないし,Double
型1つの値から Circle
コンストラクタを通して作られているかもしれない.これは実行時にその関数でパターンマッチをしてみて初めて分かることだ.型シノニムでは,型システムからそれがどういう型を元にしていたか分かるが,代数的データ型で観測できるのは新たに作られたデータ型があることだけだ.この違いは,代数的データ型と型シノニムの制約の違いに表れてくる.代数的データ型では,型シノニムの時に挙げたような制約はない.
例えば,代数的データ型は型シノニムと同様,パラメータをとることができ,さらに部分適用も可能だ 4:
+data Apply f a = Apply (f a)
+type ApplyMaybe = Apply Maybe
これは Haskell の正しいプログラムになる.Apply
は,2つのパラメータをとる型コンストラクタになっていて,データ型 Apply f a
の値を作る方法として,f a
型の値から値コンストラクタ Apply :: f a -> Apply f a
を通す方法がある.ApplyMaybe
は Apply Maybe
の型シノニムになっていて,これを使えば Apply Maybe Int
と書く代わりに ApplyMaybe Int
と書けるようになる.ApplyMaybe
の定義は,Apply
に対して1つのパラメータしか渡していない.にも関わらず正しいというのが,型シノニムと異なる点になる.
再帰的なデータ型を代数的データ型で定義することも可能だ:
+data List a
+= Cons a (List a)
+ | Nil
データ型 List a
は a
型の要素を持つ単連結リストを表す.値コンストラクタが List a
型の値を受け取ることがポイントだ.型シノニムでは,その型の定義に自身を含めることはできなかった.これは実際の具体的な型を求めようとした時,その計算が永遠に終わらなくなってしまうからだった.代数的データ型 List a
ではその型は単に新しい型として作られ,実際にその型の値がどういう型の値によって構成されているか知る必要はない.List a
はそれ自体が具体的な型であり 5 ,それ以上計算する必要はないからだ.代数的データ型において,定義された型とその型の値を作る方法は分離されている.そのため,データ型の計算においてその型の値を作る方法は考慮されない.よって,自身が定義中で用いられても,型シノニムのようにデータ型の計算が永遠に終わることがないということはないため,その操作が許容されている.
もちろん,新しい型が定義されるため,型クラスのインスタンスを混乱なく定義できる.代数的データ型を作成した時,基本的なインスタンスを定義することは Haskell プログラミングにおいてよくあることだ.Haskell では,言語機能としてそれを支援する機能がある.それは,deriving
構文というもので,Eq
/ Ord
などの標準的な型クラスを,データ型の定義から自動で導出してくれる.例えば,List a
に対して使ってみると,以下のようになる:
data List a
+= Cons a (List a)
+ | Nil
+ deriving (Eq, Ord, Show)
このように代数的データ型は,型シノニムでは定義できなかったデータ型を定義することができる.そして,代数的データ型は全く新しい型を作ることもできる:
+data Nat
+= Succ Nat
+ | Zero
このデータ型 Nat
は,他の型には依存しない全く新しい型だ.このように,代数的データ型は型シノニムと異なり全く新しい構造を作り出すことができる.
ただ,その代わり既存の関数を流用できなくなってしまう場合がある.例えば,
+data Tuple a b = Tuple a b
は,(a, b)
と構造が同じであり,(a, b)
に対する関数 fst :: (a, b) -> a
を適用できてもいいはずだ.ところが,データ型 Tuple a b
とその値コンストラクタは型システム上は切り離されているため,自身の値が (a, b)
の値と同じ方法でしか構成できないことを知らない.Tuple a b
と (a, b)
において型上で言及できることは,それらが異なる型であるということだけだ.なので,fst
に Tuple a b
型の値を渡すことはできない.これは,もし型シノニムを使って,
type Tuple a b = (a, b)
と定義した場合は解決する問題だ 6.
+このように両者にはトレードオフがあり,利用目的に合った使い分けをするのがいいだろう.
+さて,data
宣言の構文は他に2つ,便利な機能がある.
1つは正格性フラグと呼ばれる機能で,値コンストラクタにおいて引数を正格に評価することを強制できる.例えば,
+data StrictTuple a b = StrictTuple !a !b
というように,正格性フラグ !
を使った定義を行うと,値コンストラクタ StrictTuple :: a -> b -> StrictTuple
はその引数を正格に評価してから格納するようになる.通常,
data Tuple a b = Tuple a b
のように正格性フラグを使わない定義では,
+>>> case Tuple undefined undefined of Tuple _ _ -> ()
+ ()
のように値コンストラクタは受け取った引数の評価を行わず,素直にそのままの形で遅延させて格納するため,エラーを出す式を渡してもその式の評価を行わない限りエラーにはならない.これは通常の関数の動作と同じになる.ところが,正格性フラグを使用した StrictTuple
の場合,
>>> case StrictTuple undefined undefined of StrictTuple _ _ -> ()
+*** Exception: Prelude.undefined
のように引数の評価を行うため,エラーを出す式を受け取った場合値コンストラクタの適用においてその式を評価しエラーを出す.データ型を作成する際,その元となる式の評価を強制させることはパフォーマンスに大きく寄与する.そのため,そのようなことを支援するために正格性フラグは設けられている.
+また,代数的データ型の値コンストラクタはフィールド名を持つことができる:
+data Tuple a b = Tuple
+ firstVal :: a
+ { secondVal :: b
+ , }
この場合,型コンストラクタ Tuple
,値コンストラクタ Tuple :: a -> b -> Tuple a b
の他に,関数 firstVal :: Tuple a b -> a
, secondVal :: Tuple a b -> b
が作られる.また,値コンストラクタの呼び出しにおいて特別なレコード構文 Tuple { firstVal = 0, secondVal = 1 }
を使用でき,またレコード更新構文 (Tuple 2 1) { firstVal = 0 }
を使用できる.これらは両者 Tuple 0 1
と同様の値が作成される.
さて,これまで見てきたように,型シノニムは型の別名を定義し,代数的データ型は型の積和により新たなデータ型を定義するものだった.Haskell にはもう1つデータ型を定義する方法がある.それが newtype
宣言だ.この宣言によって作られるデータ型は,型システム上は代数的データ型と同じように扱われ,実行時は型シノニムと同様の動作をする.
newtype
宣言の構文は,data
宣言と同じような形をしている:
newtype Identity a = Identity a
フィールド名をつけることもできる:
+newtype Identity a = Identity
+ unIdentity :: a
+ { }
この場合 data
宣言と同様に,型コンストラクタ Identity
,値コンストラクタ Identity
が作られることになる.ただし,data
宣言と異なり newtype
は積和の機能を使用することはできない.単にある1つの型を受け取る値コンストラクタしか定義できない.なので,
newtype Unit = Unit
+newtype Tuple a b = Tuple a b
+newtype Enum = A | B | C
はいずれも受け入れられない.この newtype
の制約はいまいちよく分からない.では,このような制約によりどのような違いが出るのだろうか? newtype
と data
は型システム上は違いはない.しかし,パターンマッチの動作など,実行時の動作に少し差異が設けられている.例えば,通常
data DataIdentity a = DataIdentity a
において,
+>>> case undefined :: DataIdentity () of DataIdentity _ -> ()
+*** Exception: Prelude.undefined
のようにエラーを出す式をパターンマッチで分解しようとするとエラーが出力される.ところが,newtype
によって作られた値コンストラクタの場合,
>>> case undefined :: Identity () of Identity _ -> ()
+ ()
のようにパターンマッチ時にエラーが出されることはない.Haskell では newtype
で作られた値コンストラクタが実行動作に影響することはないと規定されている.よって,上のパターンマッチは,以下と同様の動きをすることになっている:
>>> case undefined :: Identity () of _ -> ()
+ ()
このように値コンストラクタを指定しないパターンマッチの場合,data
宣言で作られたものもエラーを出さない:
>>> case undefined :: DataIdentity () of _ -> ()
+ ()
よって,data
と newtype
で作られた値コンストラクタの動作が異なるのは,パターンマッチにおいて値コンストラクタを指定した場合だけということになる.
では,newtype
はなぜ値コンストラクタを無視するよう規定されているのだろう? これは,newtype
によるデータ型が実行時の動作として型シノニムと同様の動作をすることを目的としてしているからだ.値コンストラクタが無視されるのは,
newtype Identity a = Identity a
という宣言は,
+type IdentitySynonym a = a
という宣言と同様の意味を持って欲しいことを Haskell の設計者が意図しているからだ.よって,
+>>> case undefined :: Identity () of Identity _ -> ()
+ ()
の動作は,
+>>> case undefined :: IdentitySynonym () of _ -> ()
+ ()
のように,代数的データ型ではなく型シノニムに合わせてあるため,data
宣言主体に見ると一見不思議な動作をしていたというわけだ.
さて,ではなぜわざわざ型シノニムとは別に newtype
宣言を導入したのだろうか? 型シノニムには幾つか制約があったのを思い出して欲しい.そして,それらの制約は代数的データ型では解決されたのだった.それは type
宣言が単に型の別名を導入するのに対し,data
宣言が完全に新たな型を作るからだった.newtype
はその点に着目し,実行時には単なる別名として動作するが型システム上は完全に別の新たな型を導入することで,type
宣言同様ある型の別名を作りたいものの型シノニムの制約は回避したい需要を満たすようにしたものだ.
例えば,大文字小文字を区別しない文字列データを考えてみよう.この場合,"aBc" == "Abc"
であって欲しいが,これは型シノニムで
type CaseInsensString = String
と定義するだけでは,
+>>> ("aBc" :: CaseInsensString) == ("Abc" :: CaseInsensString)
+False
のままだ.そこで,newtype
を使って,
import qualified Data.Char as Char
+
+newtype CaseInsensString = CaseInsens String
+
+instance Eq CaseInsensString where
+CaseInsens s1 == CaseInsens s2 = go s1 s2
+ where
+ = True
+ go [] [] :_) = False
+ go [] (_:_) [] = False
+ go (_:cs1) (c2:cs2) = Char.toLower c1 == Char.toLower c2 && go cs1 cs2 go (c1
とすれば,
+>>> CaseInsens "aBc" == CaseInsens "Abc"
+True
とできる.型シノニムは単なる String
の別名なので,String
と異なるインスタンスを新しく定義することはできない.それに対して,newtype
によるデータ型は代数的データ型と同様に自由に定義することができる.そして,値コンストラクタ CaseInsens
は単なる飾りであり,実行時には完全に無視されるため,CaseInsensString
は動作としては String
の別名としてみることができる.
newtype
は型シノニムでの制約であった,
といった問題も解決する.このように newtype
は型シノニムの問題を改善したデータ型を定義するが,data
宣言と同様型シノニムでは起きなかった問題も一緒に顕在化させてしまう.
上の例で,CaseInsens
は飾りだと言ったが,実際にはこの値コンストラクタは必要不可欠であり,重要な役割を持っている.例えば,
>>> CaseInsens "aBc" == CaseInsens "Abc"
+True
の例は,片方だけ
+>>> "aBc" == CaseInsens "Abc"
としてしまうと,コンパイルエラーになってしまう.なぜなら,(==)
は2つの引数が同じ型の値である必要があり,"aBc"
の型である String
と CaseInsens "Abc
の型である CaseInsensString
は全く異なる型であるからだ.つまり,値コンストラクタ CaseInsens
は,実行時には何の影響も与えないが,型システム上は全く異なる型の値であることを示すマーカーとなる.そして,型シノニムではデータ型は単なる別名であったが,newtype
は data
と同様全く新たな型として導入する道を選んだため,元の型として受け入れてもらうことが出来なくなってしまったのだ.
といっても,これは一長一短である.data
と同様 newtype
で作られた型は,型シノニムのように既存の関数を使い回すことができない.その反面,データの意味に沿わないプログラムを型によって弾くことができるという点は長所になる場合もある.例えば,"aBc" == CaseInsens "Abc"
の例は,一体どのような結果を返すべきか一見して分からない.両者は単なる文字列と,大文字小文字を区別しない文字列という異なるデータを表しており,その比較は定義されないとするのが自然だろう.このような場合に,型シノニムでは定義されないことを表す方法はなかったが,newtype
は元の型と異なる型を持つので,そのような仕組みを作ることができる.
さて,newtype
において値コンストラクタは実行時に何の影響も及ぼさないことと,何故そうなっているかについて分かってもらえただろうか? この影響は,パターンマッチ以外にも表れる.例えば,newtype
の値コンストラクタに正格性フラグの機能はない.
newtype StrictNewtype = StrictNewtype !Int
というプログラムは,Haskell では受け入れられない.なぜなら,これを受け入れた場合,値コンストラクタがあるかどうかによって実行時の動作が変わってしまうからだ.ただ,その他の data
宣言の機能は使用できる.deriving
も使用できる.newtype
で作られたデータ型は,元のデータ型のインスタンスを継承することはできない.全く新たな型を作ったため,更地の状態から始まる.ただし,deriving
を使うことでインスタンスを用意に導出することは可能だ.ただ,標準クラスのインスタンスしか自動で導出できないため,自身で定義した型クラスなどのインスタンスは一から書く必要がある.そのことには,注意する必要があるだろう 7.
最後に少し応用的な newtype
の使い方を紹介しよう.newtype
は上のように目的に合わせて型を既存の型から作る他,型シノニムの制約によって定義できない型上の計算を実現するのにも使用できる.例えば,
newtype Fix f = Fix (f (Fix f))
という変わったデータ型を使うと,型上の不動点演算をエミュレートできる.また,newtype
を使うことで幽霊型による曖昧な型を避けることもできる.例えば,
type WithAnn ann a = a
+
+readShow :: (Read a, Show a) => WithAnn a String -> String
+= show $ read s readShow s
を考える.この関数 readShow
は,WithAnn
で引数に a
を使っているにもかかわらず a
が曖昧な型になるため弾かれる.なぜなら,型シノニム WithAnn a String
は String
と書いてるのと同じであり,readShow
は
readShow :: (Read a, Show a) => String -> String
という型を持つのと同様になってしまうからだ.このため,制約だけに a
が現れることになってしまい,曖昧な型になってしまう.この例のような,型シノニムが具体化されてしまうことで曖昧な型が生じる問題は,newtype
を使用することで回避できる:
newtype WithAnn ann a = WithAnn a
+
+readShow :: (Read a, Show a) => WithAnn a String -> String
+WithAnn s) = show $ read s readShow (
Haskell は型システム上は WithAnn a String
が実行時に単なる String
の別名として扱われることを知らず,これを1つの具体化された型として認識する.このため,実際には a
が引数の値に何ら関与しない場合も,型 a
を伴う型として残る.よって,この場合は a
は曖昧な型にならず,WithAnn a String
の a
の部分にあてがわれる型から特定することができる.このように,型シノニムで早期に元となった型に具体化されることで生じる問題は,newtype
を使うことで実際に値を作る箇所とパターンマッチの箇所での型計算に遅延させることができ,回避できる場合がある.
Haskell の3つのデータ型定義方法について紹介した.
+型シノニムは,ある型に対してその別名を与えることで,データ型を定義するものだった.簡易で元の型に対する関数をそのまま流用でき,使いやすい反面,部分適用ができない,再帰的データ型が定義できない,型クラスのインスタンスにできないと言う制約があった.
+代数的データ型は複数の型の積和によって全く新しいデータ型を定義するものだった.型シノニムであった制約を回避でき,新たな構造を導入できるが,関数の流用が困難な場合があり型シノニムとの使い分けが必要だった.
+newtype
によるデータ型は,型システム上は代数的データ型と,実行時の動作は型シノニムと同様といった,それぞれの中間をとったようなものだった.型シノニムのような関数の流用ができない場合はあるものの,その代わり型シノニムの制約を回避でき,型システム上は全く異なる振る舞いを行うことも可能だった.
これらは,それぞれが一長一短を持ち,目的にあった使い分けをする必要がある.この記事が,そのような場合の助けになればいいと思う.では,今回はこれで.
+型シノニムに対して部分適用を許容する一般的な方法は,型上にもラムダ抽象にあたる表現を導入することである.ただ,この場合型上の演算が停止しない場合があり,型システムが決定不能になる.このため,Haskell では型シノニムに対しての部分適用は許容していない.↩︎
等価再帰データ型 (equirecursive types) と呼ばれる特別な型を型システムに導入することで,このような型を許容する理論は存在するが,この理論はとても複雑で型検査のアルゴリズムも難しくなりがちである.↩︎
ただ,このような混乱が起こるかもしれないことを許容し,利便性のため型シノニムをインスタンス定義で使いたい場合,TypeSynonymInstances
という GHC 拡張を有効にすることで許容されるようになる.↩︎
型上の計算によって,実際の型が特定される型シノニムとは異なり,代数的データ型の型コンストラクタはそれ自体がもう計算できないものになる.それは部分適用されても同様であり,部分適用を許容することで型シノニムと同様の問題は起こらない.これが,代数的データ型で部分適用が許容されている理由になる.↩︎
実際にはパラメータ a
の部分に具体的な型を当てはめないといけないが,当てはめればそれは完全に具体的な型になる.↩︎
なお,代数的データ型でも型シノニムと同様の利点を手に入れるための研究は,Haskell では盛んに行われている.例えば,Generic
/ Data
型クラス,lens
パッケージなどを使うことで,構造が同じだが異なるデータ型で関数が流用できない問題を回避できる場合がある.↩︎
GHC 拡張では,deriving
構文の拡張として強力な機能がいくつか搭載されている.特に newtype
によるデータ型の場合は,GeneralizedNewtypeDeriving
や DerivingVia
拡張を使えば,インスタンスの自動導出の範囲を大幅に拡大できる.↩︎
Haskell は他のプログラミング言語には見られない特徴を多く持っている。その中の1つが純粋性だ。Haskell は純粋関数型プログラミング言語であることを、売りの1つにしている。しかし、純粋性は多くの場合表現力の縮小を招く。ところが Haskell は、IOモナドの導入により、通常のプログラミング言語と変わらぬ表現力を持てるようになっている。これは、とても驚くべきことだ。しかし、同時にこれは Haskell 入門者にとって、大きな混乱を招いているようだ。
+今回は、そもそも純粋性とはなんなのか、なぜ他の言語は純粋性を担保できないのか、そして Haskell はどうやって IO モナドにより純粋性を担保しつつ他の言語と変わらない表現力を持てるようにしているのかについて、触れていきたいと思う。
+Haskell は純粋関数型プログラミング言語 (purely functional programming language) を売りにしている。関数型 (functional) の部分は他に任せるとして、ここでは純粋 (purely) の部分に着目しよう。純粋とはなんだろうか? どういう条件を満たせば、プログラミング言語は純粋と言えるんだろうか? Haskell の公式サイト ではこう述べられている:
+++Every function in Haskell is a function in the mathematical sense (i.e., “pure”).
+Haskell の全ての関数は、数学の意味での関数 (つまり「純粋」) です。
+– haskell.org Features: Purely functional より
+
ふむ、どうやら全ての関数が、数学的な意味での関数であれば、そのプログラミング言語は純粋と言えるようだ。ところで、数学的な意味での関数とはなんだろうか? 関数が純粋とはどういうことを指すんだろうか? これは噛み砕くと、
+関数はどんな時も、同じ引数を与えられたら同じ結果を返す
+つまり、関数は毎回まっさらな状態で始まり、前にどんなことがあったのか、今巷でどういうことが起きてるのかを全く考慮に入れず、ただ受け取った引数から得られる情報だけを使って結果を計算する。
関数は、副作用を持たない
+つまり、関数は計算の結果を返す以外の役割を持たない。例えば、関数を1回呼び出すのと、関数を1回呼び出してその結果を捨てまたもう1回呼び出すので結果が変わることはない。
という2つの性質にまとめられる 1。具体的には、関数 f
が、
f 1
では 2
を返し、2回目の f 1
では 3
を返すという動作をするなら、これは 1 の条件にも 2 の条件にも違反することになる。では、関数 printString
が
()
を結果として返すことを考えよう。関数 printString
は常に ()
を返すので 1 の条件にはマッチする。しかし、この関数は、与えられた文字列をターミナルに出力するので、1回呼び出すか2回呼び出すかは重要な違いになる。よって、2 番目の条件を満たさないため、純粋ではない。逆に、関数 getNowYear
が、
となると 1 の条件に違反するため、やはり純粋ではない。関数が純粋になるためには、何の面白味もないかもしれないが、1 と 2 の条件を守らないといけない。決して夕日が沈むと突然結果を出さなくなったり、関数を呼ぶ度に近所の犬が吠えたり静かになったりしてはいけない。
+さて、このような定義なら、多くのプログラミング言語の関数が純粋性を持たないのは納得できるだろう。同じ引数でも呼ぶタイミングによって結果が変わる関数、関数を呼ぶと全く予期しなかったスイッチが作動し、別の関数の結果が突然変わるようなプログラムを思いつく人は少なくないはずだ。ただ、その人たちは同時にこうも思うだろう。
+++そのような関数はプログラミングでは必要不可欠だ。Haskell はその必要不可欠な関数を、純粋性のためだけに書けないようにしてるのだろうか?
+
その疑問は至極妥当で、当然のものだ。そして安心して欲しい。その質問に対する答えは NO だ。Haskell は純粋性を保ちながら、そのような必要不可欠な関数を表現する方法を持っている。
+Haskell には、ターミナルに出力する動作を扱う関数や、ターミナルへの入力を受け取る動作を扱う関数が存在する。これは、どうも先ほどまでの純粋性の定義とは相容れないように見える。Haskell の純粋関数型プログラミング言語という性質は、そのような関数を除いては純粋という意味なんだろうか? それなら他のプログラミング言語でも事情は変わらない。ところが、Haskell はそのような関数まで純粋なのだ。そのカラクリについて、見ていこう。
+Haskell でターミナルに文字列を (改行付きで) 出力する動作を扱う関数として、putStrLn
という関数がある。これを題材として扱っていこう。この関数はどういう型を持っているのだろう? もし、
putStrLn :: String -> ()
という型になっていたら、この場合は文字列を受け取り ()
型の値を返す関数になる。()
型は次のように定義される 2:
data () = ()
つまり、()
というたった一つの値を持つ型になる。なので、putStrLn
が putStrLn :: String -> ()
という型を持っていた場合は、常に ()
という値を返す関数となり、純粋性の 1 番目の条件を満たす。しかし、putStrLn
は呼び出し回数に応じて文字列をターミナルに出力していくので、副作用を持ち、2 番目の条件には到底当てはまらない。では、2 番目の条件に当てはまるようにするにはどうすればいいだろう? そのような選択肢は到底ないように見える。
実際には、Haskell の putStrLn
は
putStrLn :: String -> IO ()
という型を持つ。つまり、返る値は IO ()
という不思議な型を持つ。こうすると純粋に文字列をターミナルに出力できるんだろうか? 答えは NO だ。Haskell の putStrLn
関数は、
関数ではない。Haskell は、到底純粋性を持たないような操作をする関数を、そもそも関数の見方を変えて提供することで純粋性を保っている。この関数 putStrLn
は
関数だ。そして、「ターミナルにその文字列を出力する動作」は IO ()
という型を持つ。動作 (action) とは、文字通り「何をするか」 3 を表す。IO a
は、
a
であることを表す型だ。抽象的すぎてあまりピンとこないかもしれない。もし、その動作が結果を返す以外に何もしないなら、それは純粋な操作であるから、次のように書ける:
+data PureAction a = PureAction (() -> a)
つまり、引数が何もない純粋関数だ。例えば、整数を2つ受け取って、その和を計算する動作を返す関数は次のように書けるだろう:
+addAction :: Int -> Int -> PureAction Int
+= PureAction (\_ -> x + y) addAction x y
putStrLn
も addAction
と同じように、値そのものではなくその値を計算する動作それ自体を返す。ただ、putStrLn
が返す IO
の動作は、PureAction
の動作よりもっと一般的なものだ。つまり、純粋な動作ではないかもしれないということだ。もしかしたらそれは、今の時刻で結果を変えるかもしれないし、結果は常に変わらなくても何回呼び出すかでターミナルに表示する文字を変えるかもしれない。つまり、完全に純粋な関数では表せないかもしれない。でも、動作自体は不変的だ。putStrLn "str"
は
"str"
を出力する動作を表す。これが、10時にはこういう動作を返してきたのが、12時には
+"str"
を出力し、お昼の鐘を鳴らす動作を返すようになるということはないし、この動作を返す以外に
+ということもない。例えば、GHCi で以下のようなプログラムの出力を見てみよう:
+>>> let _ = putStrLn "str" in ()
+ ()
これは
+putStrLn "str"
を計算し、()
を返すというプログラムだ。このプログラムを評価しても、()
だけしか目にしないはずで、何回実行しても同じ結果が得られるはずだ。つまり、putStrLn
は余計なことを何もしていないと言えるだろう。そう説明すると、ちょっと Haskell をかじった人は
++この説明は間違っている。この式は
+putStrLn "str"
を全く評価していないので、実際にputStrLn "str"
が余計なことを何もしていないかは分からない
と言うだろう。その通りだ。この説明は間違っている。それを確認してみよう:
+>>> let _ = error "something happened!" in ()
+ ()
もし、さっきの putStrLn "str"
がちゃんと計算されていたなら、今回は something happened!
というエラーが見れるはずだ。ところが、全く何の問題もなく式の実行は終わり、()
が出力されてしまった。Haskell は遅延評価により、最終結果に本当に必要な部分しか計算してくれないので、putStrLn "str"
の部分は計算されず無視されてしまっていただけのようだ。では、ちゃんと修正してみよう。修正は、seq
という魔法の関数を使うことで可能だ。seq :: a -> b -> b
は一番最初に渡された引数を (必要かどうかに関わらず、強制的に) 計算し、その後2番目の引数を返す関数だ。この関数を使うと、次のように修正が可能だ:
>>> putStrLn "str" `seq` ()
+
+ ()>>> error "something happened!" `seq` ()
+*** Exception: something happened!
今度は大丈夫だろう。putStrLn "str"
の部分をエラーに変えると、ちゃんとエラーが出力されている。putStrLn "str"
は計算されているようだ。そう、putStrLn "str"
が実行されて実際に行われるのは、その定義通り
"str"
を出力する動作」を返すということだけで、他には何もしない。常に同じ動作を返すし、副作用を起こしたりもしない。これは純粋関数の定義に当てはまっている。putStrLn
は純粋な関数なのだ。そして、その動作には、動作の結果の型によって型が決まっていて、それが IO
型ということになる。
++では、実際に
+putStrLn
はどういう定義になるんだろう? その定義は純粋な枠組みで定義できるんだろうか?
と疑問を持つ人はいるかもしれない。その疑問はとても良いところをついている。そう、putStrLn
は、Haskell では定義できない。もしくは、定義するならば「文字列をターミナルに出力する動作」を表す値の作成方法を、何らかの仕組みで提供する必要がある。もし、
s
をターミナルに出力する動作」を PutStrLn s :: IO ()
と書けるなら、その時は、putStrLn
を次のように定義できる:
putStrLn :: String -> IO ()
+putStrLn s = PutStrLn s
ただ、今度は PutStrLn
を Haskell で定義するにはどうすればいいのだろう? という話になり、この話は延々と続くことになるだろう。現実世界の純粋なエミュレータを Haskell 内部で実装すれば収束するかもしれない。しかし、私たちは、現実世界をコストなく扱いたいわけであり、純粋かどうかは重要なことではない。そして、別に putStrLn
を Haskell 内で純粋に定義したいのではなく、「文字列 s
をターミナルに出力する動作」を扱いたいだけなのだ。なので、Haskell は純粋な部分だけは目に見える範囲で提供し、非純粋な部分は隠蔽し、純粋に扱うことだけをできるようにしている。
では、実際にこの動作を実行したい時はどうすればいいんだろう? putStrLn "str"
が純粋に、「"str"
をターミナルに出力する動作」を返してきて、それを純粋に扱うことしかできないとなると、実際にターミナルに出力することはできないのではないだろうか? それもその通りだ。では、Haskell ではその問題をどう解決するか。実は Haskell の main
プログラムは、IO
型の値で定義するようになっている。つまり、
main :: IO ()
+= putStrLn "str" main
というように、main
を何らかの IO
動作で定義する。そして、実際にこのプログラムからコンパイルされた実行ファイルは、定義された動作をそのまま行うようになっている。こうすることで、Haskell は純粋性を保ちながら、非純粋な動作を扱えるようになっている。
Haskell の putStrLn
が純粋な理由は分かってもらえただろうか? さて中には、
++主張は分かったが、純粋に扱うだけに制限するということは、普通のプログラミング言語より非純粋な動作を上手く扱えないんじゃないか
+
と疑問に思う人もいるだろう。これも当然の疑問だ。普通のプログラミング言語は、表現力豊かで、様々な制御構文を持ち、それぞれの構文が純粋性に拘らないため、とてもユニークな非純粋なプログラムを書くことができる。ただ、安心して欲しい。Haskell も、それに負けない表現力で、非純粋な動作を作成することができる。さて、Haskell は、普通のプログラミング言語の機構の基盤は
+ではないかと考えた。そして、このうまく結合する機構を、IO
動作の上で実現する方法を考えた。結果、Haskell では2つの特殊な操作が組み込まれている:
IO
動作に変換する: pure :: a -> IO a
IO
動作を繋げる: (>>=) :: IO a -> (a -> IO b) -> IO b
(>>=)
については少し説明が必要だろう。2つの IO
動作を繋げるというのは、(>>=) :: IO a -> IO b -> IO b
となった方が自然そうである。しかし、普通のプログラミング言語は、
if
文や while
文など)ということが可能だ。そして、前の動作の結果は変数束縛などにより自由に参照できる。Haskell は、IO
動作を純粋性により実際に実行することはできない。その代わり、上の動作の制御機構を、繋げる操作に組み入れることで代用しようとしたのだ。つまり、
(>>=) :: IO a -> (a -> IO b) -> IO b
の操作は、
+IO
動作を実行し、IO
動作を純粋に生成し、という動作全体を表す IO
動作を生成する。この結果から次に行う IO
動作を生成する部分をうまく定義すれば、(>>=)
によって様々な制御構文を模倣できるのではないかと考えたのだ。この仕組みはとても上手くいった。Haskell では、非純粋な動作をif
文や while
文で任意に実行することを、次のような純粋に動作を切り替える関数で代用する:
ifIO :: Bool -> IO a -> IO a -> IO a
+= case b of
+ ifIO b act1 act2 True -> act1
+ False -> act2
+
+whileIO :: (a -> Bool) -> a -> (a -> IO a) -> IO ()
+= go x0 where
+ whileIO isEnd x0 act = ifIO (isEnd x)
+ go x pure ())
+ (
+ (>>= \nx ->
+ act x
+ go nx )
これらの関数を使えば、
+main :: IO ()
+=
+ main getLine >>= \loopCmd ->
+ /= "loop")
+ ifIO (loopCmd putStrLn "No loop")
+ (-> b) (False, 0) (\(_, i) ->
+ (whileIO (\(b, _) putStrLn ("loop " ++ show i) >>= \_ ->
+ getLine >>= \loopEndCmd ->
+ == "end")
+ ifIO (loopEndCmd pure (True, i))
+ (pure (False, i + 1))
+ ( ))
のようなプログラムが書ける。このプログラムは、
+loop
と打たれれば、ループに入る。それ以外の場合は "No loop"
と出力し、プログラムを終了する。end
と打たれれば、プログラムを終了する。それ以外の場合、ループカウントを1増加させて、2 に戻る。ということを行う。このように、純粋な範囲内で繋げる操作を工夫することで、普通のプログラミング言語の機構を IO
動作内に組み込めるようになっている。ただ、このプログラムは大変見にくい。なので、Haskell はさらに、この繋げる操作を元に、次のような DSL を提供している:
main :: IO ()
+= do
+ main <- getLine
+ loopCmd /= "loop")
+ ifIO (loopCmd putStrLn "No loop")
+ (-> b) (False, 0) (\(_, i) -> do
+ (whileIO (\(b, _) putStrLn ("loop " ++ show i)
+ <- getLine
+ loopEndCmd == "end")
+ ifIO (loopEndCmd pure (True, i))
+ (pure (False, i + 1))
+ ( ))
少しは見やすくなっただろうか? この操作は、そこまで特別な操作をしてるわけではない。インデントを解析して、
+main :: IO ()
+= do
+ main <- e1
+ x1 <- e2
+ x2 e3
というのを、
+main :: IO ()
+=
+ main >>= \x1 -> do
+ e1 <- e2
+ x2 e3
に変形して、さらに
+main :: IO ()
+=
+ main >>= \x1 ->
+ e1 >>= \x2 -> do
+ e2 e3
と変形して、
+main :: IO ()
+=
+ main >>= \x1 ->
+ e1 >>= \x2 ->
+ e2 e3
と変形する、というように最初から1行1行変形して、do
がなくなるまで変形を行うだけだ。つまり一行一行の動作を (>>=)
で繋げていくのだ。なお、一番最後以外は x <- e
という形になっているのが基本で、もしそのような形になっていない e
は、_ <- e
と変換される 4 。なので、
main :: IO ()
+= do
+ main putStrLn "str1"
+ putStrLn "str2"
は、
+main :: IO ()
+= do
+ main <- putStrLn "str1"
+ _ putStrLn "str2"
と変換された後、先ほどの変換によって、
+main :: IO ()
+=
+ main putStrLn "str1" >>= \_ ->
+ putStrLn "str2"
となる。このようにして、Haskell は他のプログラミング言語の非純粋な動作を、純粋な枠組みでも同じように扱えるようになっている 5 。Haskell は、この仕組みをモナディックIO
と名付け、IO
型を IO
モナドと呼んでいる。モナドとは何か、どういう便利な側面があるのかについては、他の記事に譲る。
どうやら、Haskell の IO
動作の仕組みが、純粋な枠組みでも他のプログラミング言語とそう劣るものではないということが分かってもらえただろうか? ところで、先ほどの ifIO
や whileIO
は、IO
動作を何事もなく引数にとって返したりしていた。ifIO
の定義をもう一度よく見てみよう:
ifIO :: Bool -> IO a -> IO a -> IO a
+= case b of
+ ifIO b act1 act2 True -> act1
+ False -> act2
このプログラムは、条件を表す引数と、IO
動作を2個受け取り、条件によって2つの動作のうちのどちらかを返していた。これは考えてみれば、とても不思議で強力なことだと思わないだろうか? 普通のプログラミング言語の if
文は、条件から書かれたプログラムのどちらかを実行する。一方、ifIO
は実行を制御しているわけではない。単に、普通の関数と同じように、2つの動作を受け取って、そのうちの片方を関数の返り値として返すだけだ。ifIO
を呼び出したプログラマは、返ってきた動作をゴミ箱に捨ててもいいし、(>>=)
で繋げて「2回続けて同じ動作をする」1つの動作にしてもいい。もちろんその動作も main
に組み入れるかはプログラマ次第だ。なんなら、main
以外にライブラリの一部としてグローバルに定義してもいい。ライブラリを使うユーザは、やっぱりそれを使うも使わないも自由だ。main
に組み入れない限り、その動作は単なるデータであり、実行もされない。
IO
動作がデータであることは、プログラムをより豊かにする。さっきの ifIO
は、条件によって片方の動作を返していた。IO
動作はもっと多彩に制御できる。例えば、条件によって動作の順番を変えたかったら次のように書けばいい:
chooseOrderIO :: Bool -> IO a -> IO a -> IO a
+= case b of
+ chooseOrderIO b act1 act2 True -> do
+
+ act1
+ act2False -> do
+
+ act2 act1
chooseOrderIO
は条件によって、受け取った動作を実行する順番を変え、その順序で結合した動作を返す。順番が同じで結果だけ選ぶといったこともできる:
ifResultIO :: Bool -> IO a -> IO a -> IO a
+= do
+ ifResultIO b act1 act2 <- act1
+ x1 <- act2
+ x2 case b of
+ True -> x1
+ False -> x2
ifIO
は条件によって動作そのものを選んでいたが、ifResultIO
はどの条件でも act1
と act2
の順に動作をすること自体は変えない。代わりに、その動作の結果をどっちにするかだけを変える。このように、Haskell は IO
動作を、多彩に、しかも純粋にコーディネートすることができる。これは、他の多くのプログラミング言語にはなく、しかも強力な機能だ。そう、Haskell の IO
動作は、それが単なるデータであるがゆえに、通常のプログラミングの範囲で自由に加工できるのだ。
これを、動作が第一級であるという。第一級とは、つまり他のデータと全く同じように扱えるということだ。
+この記事では、
+について紹介した。どうだろう? Haskell の IO モナドについて、少しでも理解の補助になっただろうか?
+Haskell の IO モナドとは、動作そのものを値に持つ型だった。そして、その値は、特別な繋げる操作により他の言語と同じように加工でき、しかも通常のプログラミングの範囲で加工が可能になっている。しかも、加工自体は純粋にでき、動作の生成も純粋にできる。これが、Haskell が純粋であると言われる所以だった。
+この Haskell の根幹をなす機能が、どういう点で魅力的なのか分かってもらえたら、この記事を書いた甲斐があるというものだ。もし、あなたもこの機能の魅力に取り憑かれたらなら、ぜひ IO
動作をふんだんに加工してプログラミングをしていって欲しい。では、楽しい Haskell ライフを。
ところで、もしかしたら、読者の中には、
+++Haskell の IO モナドは、現実世界を状態にする State モナドだ
+
という主張を、見たことがある人がいるかもしれない。最後におまけとしてこの話に触れておこうと思う。気になる人は、この後も呼んでみると、IO
モナドの理解の助けになるかもしれない (または、むしろ混乱するかもしれない。もし、混乱したなら、とりあえずこの話は忘れることをお勧めする。ここに書いてある話を理解しなくても、IO
モナドの利用に関して全く支障はない。そういう話もあるぐらいの事柄だ。なので、安心してまずは Haskell プログラミングを楽しんでほしい。いつか楽しみ飽きたら戻ってきてもいいかもしれない)。
まず、この話は、
+を押さえておいて欲しい。さて、Haskell の代表的な処理系 GHC は、標準の範囲では純粋関数型プログラミングを提供するが、全体としては非純粋な計算も許容している。そして、その計算を IO
モナドの内部に使っている。GHC では IO
モナドは、通常の言語内の一部として定義されている:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
この型は、詳細は省くが、本質的には、
+State# RealWorld
型の値を受け取り、State# RealWorld
型の値と a
型の値のタプルを返す関数の newtype
になっている。State
モナドについて知ってる人は、これは State (State# RealWorld) a
と同じだと思うだろう。しかし、IO a
の値は、他に Haskell の型では表現できない契約を持つ。それは、
State# RealWorld
の型の値は、必ず1回だけ使用されるという契約だ。なので、
+IO $ \s# -> (# s#, \() -> s# #)
は 6 s#
を2箇所で使ってるため IO (() -> State# RealWorld)
の値になれないし、
IO $ \s# -> (# s#, IO $ \_ -> (# s#, () #) #)
は一番外側の IO
は s#
を 2 箇所で使っているため契約違反で、内側の IO
は受け取った引数を一度も使っていないのでやはり契約違反ということになる。この定義を使って、例えば Haskell で可変参照を扱う IORef
のフレームワークは、次のように定義されている 7 :
data IORef a = IORef (MutVar# RealWorld a)
+
+newIORef :: a -> IO (IORef a)
+init = IO $ \s1# -> case newMutVar# init s1# of
+ newIORef # s2#, var #) -> (s2#, IORef var)
+ (
+readIORef :: IORef a -> IO a
+IORef var) = IO $ \s1# -> readMutVar# var# s1#
+ readIORef (
+writeIORef :: IORef a -> a -> IO ()
+IORef var#) val = IO $ \s1# -> case writeMutVar# var# val s1# of
+ writeIORef (# -> (# s2#, () #) s2
この定義は、ちゃんと IO
型の制約を守っている。なおここで出てくる、#
が付く関数やデータ型は、GHC の中で特別扱いされ、プリミティブな関数やデータ型になっている。それぞれ、以下の型の関数として扱える:
# :: a -> State# s -> (# State# s, MutVar# s a #)
+ newMutVar
+# :: MutVar# s a -> State# s -> (# State# s, a #)
+ readMutVar
+# :: MutVar# s a -> a -> State# s -> State# s writeMutVar
注意して欲しいのは、これらの関数は純粋ではないということだ。これは、GHCi 上で次のように確認できる:
+>>> :set -XMagicHash -XUnboxedTuples
+>>> :module GHC.Prim GHC.Types
+>>> :{
+IO $ \r0# ->
+let (# r1#, var# #) = newMutVar# False r0# in
+ let (# r2#, b1 #) = readMutVar# var# r1#
+ # = writeMutVar# var# True r2#
+ r3in
+ let (# _, b2 #) = readMutVar# var# r1# in
+ # r3#, b1 == b2 #)
+ (:}
+False
b1
と b2
は両方とも readMutVar# var# r1#
から得た値になる。ところが、これらを比較してみると False
になる 8 。もし、readMutVar#
が純粋なら、b1
と b2
の結果は同じになるため、上の評価結果は True
になるはずだ。しかし、残念ながら readMutVar#
は純粋ではないので、b1
と b2
は異なる値になってしまう。なお、この式は、IO
型で定義しているが、実際には
readMutVar#
の呼び出しで r1#
を 2 回使用しているし、State# RealWorld
の値を捨てているので契約違反であることに注意だ。GHCi 上で、うまく評価結果を確認するために、IO
を使っている。
さて、純粋性を守れないなら、GHC は一体全体何のためにこのような定義をしているんだろう? 関数が純粋でなくてもいいなら、単に
+newtype IO a = IO (() -> a)
と定義しても問題ないのではないだろうか? この方が自然に動作を表しているように思える。ところが、このような定義は、ある問題を招くのだ。Haskell は純粋関数型プログラミング言語という売りの他に、遅延評価という他の言語にはあまり見られない評価機構を採用している。もちろん、GHC も遅延評価が基本だ。正確には、Haskell の評価順序は、
+++The order of evaluation of expressions in Haskell is constrained only by data dependencies; an implementation has a great deal of freedom in choosing this order.
+Haskell の式の評価順序は、データ依存によってのみ縛られます。これは、実装がこの順序の選択において、大きな自由度を持つことを意味します。
+ +
とあるように、データ依存関係によってのみ制御できる。ところが、IO
動作は違う。例えば、
main :: IO ()
+= do
+ main putStrLn "str1"
+ putStrLn "str2"
という式において、putStrLn "str1"
と putStrLn "str2"
の動作の間には、何らのデータ依存関係も存在しない。しかしながら、main
は
"str1"
をターミナルに出力した後、"str2"
をターミナルに出力するという動作を表して欲しいはずだ。つまり、IO
動作はデータ依存関係によってのみ順序が決まるわけではなく、その繋げ方によって順序が決まって欲しいのだ。ところが、単純に
newtype IO a = IO (() -> a)
という定義を採用してしまうと、IO
の中身は繋げ方の順序を情報として持たないため、動作の実行順序を制御するのに、別途工夫が必要になる。そこで、元の定義の登場だ:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
この State# RealWorld
は、実際には ()
型と同じく単一の値を持つほぼ何の意味も持たない型になる。しかし、この型の値を計算度に付与することで、データ依存を作ることができる。具体的には、この IO
に対して次のように pure
/ (>>=)
を定義するのだ:
pure :: a -> IO a
+pure x = IO $ \r# -> (# r#, x #)
+
+(>>=) :: IO a -> (a -> IO b) -> IO b
+IO f >>= g = IO $ \r0# ->
+let (# r1#, x #) = f r0#
+ IO g' = g x
+ in g' r1#
特に、(>>=)
の定義が重要になる。(>>=)
が返してくる IO
の中身は、
State# RealWorld
をまず最初の IO
動作に渡すg
に渡して、次の IO
動作を生成するIO
動作に、最初の IO
動作が返してきた State# RealWorld
を渡すということを行っている。これにより、
+State# RealWorld
f
の結果の State# RealWorld
g'
の結果の State# RealWorld
という順で State# RealWorld
のデータ依存関係が出来上がる。つまり、通常の Haskell の評価の枠組みで、評価順序を保証できるようになるのだ。実際に、IORef
を例に考えてみよう。
main :: IO Bool
+= do
+ main <- newIORef False
+ x <- readIORef x
+ b1 True x
+ writeIORef <- readIORef x
+ b2 pure $ b1 == b2
というプログラムにおいて、最終的な pure $ b1 == b2
からのデータ依存関係に、一見 writeIORef True x
は関与していないように見える。ところが、内部を見てみると、上のプログラムは、
main :: IO Bool
+= IO $ \r0# ->
+ main let (# r1#, x# #) = newMutVar# False r0#
+ # r2#, b1 #) = readMutVar# x# r1#
+ (# = writeMutVar# True x# r2#
+ r3# r4#, b2 #) = readMutVar# x# r3#
+ (in (# r4#, b1 == b2 #)
と同じ意味を持ち、全て隠れた State# RealWorld
によってデータ依存で紐づいている。もちろん、let
内の定義の順番を変えても何の問題もない。重要なのは r0#
から r1#
の結果が得られ、r1#
から r2#
の結果が得られ、というようなデータ依存だけだ。これにより、GHC は Haskell の評価順序に特別な規則を設けない 9 10 で、IO
を実装しているのだ。
なお、こうなると IO
の
State# RealWorld
の型の値は、必ず1回だけ使用されるという契約も意義が見えてくる。もし、この契約が破られると、途中でデータ依存が分岐したり、または途中で途絶えたりすることになる。そうなると、動作がどういう挙動をするかは、Haskell 内では規定されなくなってしまう。実際に、最適化によってどう動作するかが変わってきてしまう例も作れる。IO
の契約とは、データ依存が必ず一本の線で繋がり、Haskell の評価の枠できちんと順番が規定されるということを保証しているのだ。
これが、GHC がこのような定義を IO
で採用している理由になる。もちろん、アナロジーとして現実世界全体を表す架空の状態を State# RealWorld
と見立て、IO
動作の実行により新たな現実世界全体の状態が手に入るという見方は可能だ。名前の由来もそこから来ている。ただ、基本的には、GHC において、特別な仕組みを入れずに IO
を実装するためのやり方であるということを押さえておいて欲しい。
定義は、 School of Haskell のチュートリアル から拝借している。↩︎
この定義は、Haskell Language Report 6.1.5 The Unit Datatype で述べられているが、実際には Haskell の構文規則に違反している特別な構文が使われている。なので、実際に Haskell でこのように定義できるわけではなく、擬似的に書くとこうなるという意味になる。↩︎
動作は、計算 (computation) とも呼ばれる。また、日本の Haskell コミュニティでは、英語そのままで「アクション」とも呼ばれている。↩︎
厳密には、(>>)
という別の関数を使って定義されるんだが、意味的にはそのような変換と思ってもらって構わない。正式な変換方法は、Haskell Language Report 3.14 Do Expressions を参照するといいだろう。↩︎
多くの言語では、main
プログラム以外の、例えばライブラリが勝手にスレッドを1つ立てるなどの挙動をサポートしている。そのような挙動は、Haskell では残念ながらできない。なぜなら、Haskell では main
に動作を組み入れない限りその動作は実行されないからだ。この点では、他の言語より表現力は劣っているということもできる。しかし、そのような機能は、多くの場合明示的に模倣できる。↩︎
(# x, y #)
は (# a, b #)
型の値を表す特別な構文だ。ここでは詳細は述べないので、x
と y
のタプルの特別な表記方法だと思ってもらって構わない。↩︎
実際には ST
モナドとの兼ね合いで、直接こう定義はされていないが、分かりやすさのため簡略化している。↩︎
実際には、最適化次第で結果が変わることもある。↩︎
さらに、State# RealWorld
は unlifted なデータ型になっており、サンクを持たない。このため、強制的に正格評価になるようになっており、IO
動作が遅延され、最後に一気に評価されるということを防いでいる。これも、通常の GHC の枠組みの中で提供されているのは、とても興味深い。↩︎
厳密には、残念ながら全てを特別扱いせずに済ませられているわけではない。GHC では、State# RealWorld
に関して一部の最適化で特別な処理を施している。↩︎
タイトルがほとんどすべてなんですが詳細を解説します。
+shouldBe
などはproperty
の中でも使えるので使ってください!みなさんはHspecでQuickCheckを使ったproperty testを書く際、どのように書いているでしょうか?
+例えばHspecのマニュアルのように、Hspecにproperty testを組み込む例として、次のような例を挙げています。
"read" $ do
+ describe "is inverse to show" $ property $
+ it -> (read . show) x == (x :: Int) \x
※こちらのコミットの時点での話です。
+property
関数に渡した関数(以下、「porperty
ブロック」と呼びます)の中ではHspecでおなじみのshouldBe
などのexpectation用関数を使わず、==
で結果を判定してますよね。
+このサンプルに倣って、Hspecで書いたテストにproperty testを書くときは、==
を使ってる方が多いんじゃないでしょうか?
ところが、この記事のタイトルに書いたとおり、実際のところproperty
ブロックの中でもshouldBe
は利用できます。
+つまりは、こちら👇のようにも書ける、ということです!
"read" $ do
+ describe "is inverse to show" $ property $
+ it -> (read . show) x `shouldBe` (x :: Int) \x
このようにproperty
ブロックの中でもshouldBe
やshouldSatisfy
といった、Hspec固有のexpectation関数を使うことの利点は、単に構文を他のテストと一貫させることができる、だけではありません。
+テストが失敗したときのエラーが分かりやすくなる、という遥かに重大なメリットがあるのです。
試しにわざとテストを失敗させてみましょう。
+先ほどの例:
"read" $ do
+ describe "is inverse to show" $ property $
+ it -> (read . show) x == (x :: Int) \x
における(x :: Int)
という式を(x + 1 :: Int)
に変えれば、必ず失敗するはずです。
"read" $ do
+ describe "is inverse to show" $ property $
+ it -> (read . show) x == (x + 1 :: Int) \x
※お手元で試す場合はこちらから元のコードを持ってきて、stack build hspec
なりを実行した上で修正・実行するのが簡単でしょう。
結果、下記のようなエラーメッセージとなるでしょう。
+...
+ 1) read, when used with ints, is inverse to show
+ Falsifiable (after 1 test):
+ 0
+このエラーでは「テストが失敗したこと」と「どんな入力をQuickCheckが生成したか」までしか教えてくれず、わかりづらいですよね。
+一方、shouldBe
を使用して以下のように書き換えると…
"read" $ do
+ describe "is inverse to show" $ property $
+ it -> (read . show) x `shouldBe` (x + 1 :: Int) \x
エラーメッセージはこう👇なります。
+ 1) read, when used with ints, is inverse to show
+ Falsifiable (after 1 test):
+ 0
+ expected: 1
+ but got: 0
+「テストが失敗したこと」と「どんな入力をQuickCheckが生成したか」に加えて、shouldBe
に与えた両辺の式がどのような値を返したか、まで教えてくれました!
+今回の例は極めて単純なのであまり役に立たないかも知れませんが、あなたが書いた関数をテストするときはやっぱり「期待される結果」と「実際の結果」両方がわかる方がデバッグしやすいですよね!
と、いうわけで今後はproperty
関数(あるいはその省略版のprop
関数)に渡した関数の中でもshouldBe
などを必ず使ってください!
+(せっかくなんで、今回紹介したドキュメントを修正するためのPull requestを送っておきました。これがマージされればこの記事の情報の大半は時代遅れになります)
しかしここで、一つ疑問が残ります。
+QuickCheckやHspecのドキュメントをつぶさに読んだことがある方はお気づきでしょう。
+QuickCheckのproperty
関数は、Testable
という型クラスのメソッドであるため、Testable
のインスタンスでなければ使えないはずです。
+HspecのshouldBe
などが返す値は型シノニムのたらい回しをたどればわかるとおり、結局のところIO ()
型の値です。
+ところがTestable
のインスタンス一覧を見る限り、IO a
はTestable
のインスタンスではありません。
+先ほどの例のように
$ \x -> (read . show) x `shouldBe` (x + 1 :: Int) property
と書いた場合における、関数型(a -> prop)
のインスタンスは、(Arbitrary a, Show a, Testable prop) => Testable (a -> prop)
という定義のとおり、関数の戻り値の型がTestable
のインスタンスでないと、型チェックを通らないはずです。
+Testable
のインスタンスでない、IO ()
を返しているにも関わらず型エラーが起きなかったのは、一体なぜでしょうか?
その秘密を探るべく、GHCiを立ち上げましょう。
+先ほどの例のソースコードをghci
コマンドに読ませれば、まとめてHspecのモジュールもimport
できるので簡単です。
> stack exec ghci .\QuickCheck.hs
GHCiが起動したら、:i Testable
と入力して、Testable
型クラスのインスタンス一覧を出力しましょう。
> :i Testable
+class Testable prop where
+ property :: prop -> Property
+{-# MINIMAL property #-}
+ -- Defined in ‘Test.QuickCheck.Property’
+ instance [safe] Testable Property
+-- Defined in ‘Test.QuickCheck.Property’
+ instance [safe] Testable prop => Testable (Gen prop)
+-- Defined in ‘Test.QuickCheck.Property’
+ instance [safe] Testable Discard
+-- Defined in ‘Test.QuickCheck.Property’
+ instance [safe] Testable Bool
+-- Defined in ‘Test.QuickCheck.Property’
+ instance [safe] (Arbitrary a, Show a, Testable prop) =>
+Testable (a -> prop)
+ -- Defined in ‘Test.QuickCheck.Property’
+ instance [safe] Testable ()
+-- Defined in ‘Test.QuickCheck.Property’
+ instance [safe] Testable Test.HUnit.Lang.Assertion
+-- Defined in ‘Test.QuickCheck.IO’
ありました!💡
+最後の方にあるinstance [safe] Testable Test.HUnit.Lang.Assertion
という行に注目してください。
+Test.HUnit.Lang.Assertion
は、IO ()
の型シノニムであり、Hspecでも間接的に型シノニムとして参照されています1。
+要するにinstance [safe] Testable Test.HUnit.Lang.Assertion
という行はinstance [safe] Testable (IO ())
と読み替えることができます([safe]
という表記が指しているものについてはここでは省略します!すみません!)。
紹介したとおりTestable
のドキュメントにはTestable Assertion
なんて記載はありませんし、じゃあ一体どこで定義したのか、というとそう、続く行に-- Defined in ‘Test.QuickCheck.IO’
と書かれているとおり、Test.QuickCheck.IO
というモジュールで定義されています!
Test.QuickCheck.IO
は、名前のとおりQuickCheckのTestable
について、IO
のorphan instanceを定義するためのモジュールです。
+これをimport
しているが故に、Hspecではproperty
ブロックの中でshouldBe
などが利用できるんですね!
結論:
+:i
はorphan instanceであろうとインスタンスを定義した箇所を見つけてくれるから便利!この節の冒頭で「型シノニムのたらい回し」と呼んだものを追いかけてみましょう。
+おなじみshouldBe
はExpectation
という型の値を返します。
+そしてExpectation
はAssertion
の型シノニムであり、クリックするとTest.HUnit.Lang.Assertion
であることがわかります。
+そしてAssertion
はそう、type Assertion = IO ()
とあるとおりIO ()
なのです。やっと知ってる型にたどり着きました😌。↩︎
Haskell-jpのコンテンツの一つとしてHaskell AntennaというWebページの開発・運用をしております。
+2019年の今頃、これを自動毎時更新しようと Drone Cloudによる毎時更新を設定しました。
+しかし。。。なんと去年の3月ぐらいからこれが止まっています(どうやら、Drone Cloudのこの機能を利用してマイニングをした人がいたらしく止めてしまったようです)。 +現在は僕がだいたい毎朝1回、手動でCIを回しています。。。
+ずっとなんとかしなきゃなぁと思い続けてはや9ヶ月。 +やっと重い腰をあげてなんとかしました! +というよりは、なんとかする方法を思い付いたので実装してみました。
+GCPにはalways freeプランというのがあり、GCEインスタンスの場合はf1-microであれば一台だけ無料です(2020/1現在)。 +これに、毎時実行して更新をプッシュするantennaプログラムを仕込んでおけば良いではないかということに気づきました。
+Haskell Antenna自体はGitHub Pagesであり、HTMLなどはhaskell-jp/antennaという Haskell製CLIアプリケーションで生成しています。 +これをcronか何かで毎時実行すればいいんですけど
+という問題があります。
+そこで、(2) のプッシュの部分も含めて毎時実行の処理をantennaアプリケーションに閉じ込めてしまえば、docker run
しておくだけで良いのではないか?というのを思い付きました!
+ということで、そういう風にantennaを改良します。
antennaプログラムに「gitコマンドを使ってGitHubリポジトリに更新をプッシュする機能」と「全てを毎時実行する機能」の2つを組み込む必要があります。 +ここで後方互換性を維持するために、これらはオプションでオンする機能にしましょう。 +なのでまずは、antenna CLIアプリケーションのオプションを整理するところから始めます。
+改修前のantennaはオプションを持っていません。
+getArgs
で引数(設定ファイルのパス)を受け取るだけです
import System.Environment (getArgs)
+
+-- generate 関数が設定から HTML ファイル群を生成する IO アクション
+main :: IO ()
+= (listToMaybe <$> getArgs) >>= \case
+ main Nothing -> error "please input config file path."
+ Just path -> generate path =<< readConfig path
これを extensible の GetOpt
を使ってオプションを貰えるように拡張します
-- withGetOpt' は usage を独自で扱えるように拡張した Data.Extensible.withGetOpt です
+main :: IO ()
+= withGetOpt' "[options] [input-file]" opts $ \r args usage ->
+ main if | r ^. #help -> hPutBuilder stdout (fromString usage)
+ | r ^. #version -> hPutBuilder stdout (Version.build version)
+ | otherwise -> runCmd r $ listToMaybe args
+ where
+ = #help @= helpOpt
+ opts <: #version @= versionOpt
+ <: #verbose @= verboseOpt
+ <: nil
+
+type Options = Record
+"help" >: Bool
+ '[ "version" >: Bool
+ , "verbose" >: Bool
+ ,
+ ]
+helpOpt :: OptDescr' Bool
+= optFlag ['h'] ["help"] "Show this help text"
+ helpOpt
+versionOpt :: OptDescr' Bool
+= optFlag [] ["version"] "Show version"
+ versionOpt
+verboseOpt :: OptDescr' Bool
+= optFlag ['v'] ["verbose"] "Enable verbose mode: verbosity level \"debug\"" verboseOpt
差分全体はこのPRで確認することができます。
+興味のある人はみてみてください。
+generate
関数は以下の runCmd
関数から呼ばれています
import Mix
+import Mix.Plugin.Logger as MixLogger
+
+runCmd :: Options -> Maybe FilePath -> IO ()
+Nothing = error "please input config file path."
+ runCmd _ Just path) = do
+ runCmd opts (<- readConfig path
+ config let plugin = hsequence
+ $ #logger <@=> MixLogger.buildPlugin logOpts
+ <: #config <@=> pure config
+ <: nil
+ $ generate path
+ Mix.run plugin where
+ = #handle @= stdout
+ logOpts <: #verbose @= (opts ^. #verbose)
+ <: nil
runCmd
関数はmix.hsを使って RIO env ()
のボイラーテンプレートを減らしています。
Haskellアプリケーションからgitコマンドを実行するにはShellyを使うことにします。 +Shellyはmix.hsのshellプラグインを使うことで簡単に使用することができます。 +まずはコミットを作る部分を実装しましょう
+import qualified Git -- 自作Shelly製gitコマンド関数群
+import qualified Mix.Plugin.Shell as MixShell
+
+runCmd :: Options -> Maybe FilePath -> IO ()
+Just path) = do
+ runCmd opts (<- readConfig path
+ config let plugin = hsequence
+ $ #logger <@=> MixLogger.buildPlugin logOpts
+ <: #config <@=> pure config
+ <: #work <@=> pure "."
+ <: nil
+ $ do
+ Mix.run plugin ^. #withCommit) $ MixShell.exec (Git.pull [])
+ when (opts
+ generate path^. #withCommit) $ commitGeneratedFiles
+ when (opts where
+ = ...
+ logOpts
+commitGeneratedFiles :: RIO Env ()
+= do
+ commitGeneratedFiles <- view #files <$> asks (gitConfig . view #config)
+ files $ do
+ MixShell.exec
+ Git.add files<- Git.diffFileNames ["--staged"]
+ changes not $ null changes) $ Git.commit ["-m", message]
+ when (where
+ = ... message
全ての差分はこのPRから確認できます。
+runCmd
関数に追記したのは when (opts ^. #withCommit)
から始まる2行です(Options
に #withCommit
を追加しています)。
+mix.hsのshellプラグインを使うことでShellyのログをだいたいそれっぽくrioのロガーに流してくれます。
次に、git push
も実装します
runCmd :: Options -> Maybe FilePath -> IO ()
+Just path) = do
+ runCmd opts (...
+ $ do
+ Mix.run plugin ^. #withCommit) $ MixShell.exec (Git.pull [])
+ when (opts
+ generate path^. #withCommit) $ commitGeneratedFiles
+ when (opts ^. #withPush) $ pushCommit
+ when (opts
+pushCommit :: RIO Env ()
+= do
+ pushCommit <- view #branch <$> asks (gitConfig . view #config)
+ branch "origin", branch]) MixShell.exec (Git.push [
前から使っている gitConfig
は設定ファイルからgitコマンドに関する設定を取ってきています(例えば、どのファイルをコミットするかやどのブランチにプッシュするかなど)。
これで、差分があった場合はgit commit
を実行し、最後にgit push
するようなオプション、--with-commit
と--with-push
を実装できました(他にも実装していますが割愛)。
メインディッシュである毎時実行です。 +Haskell-jp Slackで、スケジューリング実行をHaskellアプリケーション内で行うのにちょうど良いパッケージはありますか?と尋ねたところcronというパッケージを紹介してもらいました(名前がややこしい笑)。 +調べてみたところ、ちょうど良さそうなのでこれを使うことにします
+import System.Cron (addJob, execSchedule)
+
+main :: IO ()
+= withGetOpt' "[options] [input-file]" opts $ \r args usage ->
+ main if | r ^. #help -> hPutBuilder stdout (fromString usage)
+ | r ^. #version -> hPutBuilder stdout (Version.build version)
+ | r ^. #hourly -> runCmd r (listToMaybe args) `withCron` "0 * * * *"
+ | otherwise -> runCmd r (listToMaybe args)
+ where
+ = ...
+ opts
+withCron :: IO () -> Text -> IO ()
+= do
+ withCron act t <- execSchedule $ addJob act t
+ _ $ threadDelay maxBound -- 無限ループ forever
全ての差分はこのPRから確認できます。 +すっごい簡単ですね。 +ついでに、毎日実行と毎分実行するオプションも追加しています。
+これでアプリケーションの方は出来上がったので、こいつをGCEインスタンスで動作させてみましょう。
+まずはGCP Consoleからインスタンス作成します。 +構成は次の通りです
+GCP ConsoleからSSHして、docker コマンドをインストールします(やり方は公式サイトのをそのまま)。
+ここまでできたら試しに sudo docker pull haskelljp/antenna
して最新のイメージを取得してみましょう。
次に、GitHubにプッシュするためにSSH Keyを生成してデプロイキーを haskell-jp/antenna リポジトリに設定します。
+できたら適当に git clone git@github.com:haskell-jp/antenna.git
してブランチを gh-pages
に切り替えます。
あとは次のコマンドでantennaプログラムを実行するだけです
+$ sudo docker run -d \
+ -v `pwd`:/work
+ -v `echo $HOME`/.ssh:/root/.ssh \
+ haskelljp/antenna antenna --verbose --with-commit --with-push --with-copy --hourly sites.yaml
+docker logs
を使って様子をみてましたが、うまくいってるようです!
igrep氏がIssueにしてくれてるように、Haskell Antennaの正しい差分をHaskell-jp Slackに通知する仕組みを整備しようと考えてます。
+実はコミットをHaskellアプリケーション内で組み立てるようになった結果、Haskellアプリケーション側でいい感じに差分を調べ上げ、その結果をコミットメッセージに組み込むことができるようになりました。
+さすがにHTMLやフィードの git diff
を解析するのは大変なので、いい感じに各サイトの最終更新ログを残すようにしてみようかなって考えてます。
Haskellは他の多くのプログラミング言語と異なった特徴を備えており、しばしばそれらが議論を呼ぶことがあります。その中でも特によく俎上に上がるのが、遅延評価です。遅延評価は、適切に扱えば不要な計算を行わず、計算資源を節約してくれるステキな仕組みですが、一歩使い方を間違うと「サンク」という「これから実行する(かも知れない)計算」を表すオブジェクトが無駄に作られてしまい、却ってメモリー消費量が増えてしまう、などといった問題を抱えています。この現象は「スペースリーク」と呼ばれ、かつて専門のAdvent Calendarが作られたことがあるほど、Haskeller達の関心を集めてきました。
+そんなHaskeller達の悩みの種を軽減しようと、GHC 8.0以降、Strict
とStrictData
という言語拡張が搭載されました。これらの拡張は、大雑把に言うと、
StrictData
: 値コンストラクターにおいて、引数の値が弱頭正規形(Weak Head Normal Form。以降慣習に従い「WHNF」と呼びます)まで評価されるようになるStrict
: StrictData
の効果に加え、あらゆる関数の引数やローカル変数の定義において、パターンマッチで代入した変数の値がWHNFまで評価されるようになるというものです。
+このうち、StrictData
は比較的リスクが少なく大変有用(もはや標準であって欲しいぐらい)という声をよく聞きますが1、Strict
については様々な問題点があることが知られています。今回はその各種問題点をまとめて共有することで、思い切ってStrict
を有効にするときに参考になる情報を提供したいと思います!
以下の知識について、ざっくり理解しているものとして進めます。参考になりそうな日本語のページも付記したので、ご覧ください。
+BangPatterns
について
+Strict
とStrictData
について
+これから紹介するコードは、すべてこのブログのリポジトリーの、examples
ディレクトリーに置いておきました。下記のコマンドを実行すれば実際に試すことができます(一部実行する際のコマンドが異なりますので、適宜例示します)。
git clone https://github.com/haskell-jp/blog.git
+cd blog/examples/2020/strict-gotchas
+stack exec runghc -- <これから紹介するコードのファイル>.hs
+実際に試すときは--ghc-arg=-XStrict
というオプションをrunghc
に付けた場合と付けなかった場合両方で実行して、違いを確かめてみてください。
なお、使用したGHCのバージョンは8.10.1で、OSはWindows 10 ver. 1909です。
+where
句だろうとなんだろうと評価サンプル: where.hs
+最初のケースは、遅延評価で当たり前に享受できていたメリットが、Strict
を有効にしている状態では得られなくなってしまう、というものです。pxfncさんのStrict拡張でハマったお話という記事でも紹介されてはいますが、まとめ記事なのでここでも改めて取り上げます。
= print $ div10 0
+ main
+div10 :: Int -> Int
+
+ div10 n| n == 0 = 0
+ | otherwise = result
+ where
+ = 10 `div` n result
ご覧のとおり、本当にほとんどpxfncさんの記事のサンプルそのままで恐縮ですが、このプログラム、👇のようにStrict
拡張を有効にして実行するとエラーが起こります。
> stack exec -- runghc --ghc-arg=-XStrict where.hs
+where.hs: divide by zero
一方、Strict
拡張を有効にしなかった場合、エラーは起こりません。
> stack exec -- runghc where.hs
+0
なぜこんなことが起こるのでしょう?
+これは、Strict
拡張がパターンマッチで代入したあらゆる変数の値をWHNFまで評価するようになった結果、where
句で代入した変数まで必ずWHNFまで評価してしまうために発生したエラーです。すなわち、where
における、
= 10 `div` n result
までもが、
+!result = 10 `div` n
とBangパターンを付けた代入であるかのように解釈されたのです2。
+こうなると、result
を使用しないケース、すなわちn == 0
の場合であってもresult
に (WHNFまで評価した)値を代入するのに必要な計算は実行され、結果10 `div` 0
が計算されようとしてdivide by zero
が発生するのです。
⚠️where
句は関数定義の後ろの方に書くという性格上、見落としがちかも知れません。注意しましょう。
サンプル: const.hs
+続いて、Haskellに慣れた方なら誰もが一度は試したくなる、ポイントフリースタイルに関する落とし穴です。まずは次の二つの関数をご覧ください。
+dontReferArgs :: a -> b -> a
+= const
+ dontReferArgs
+referArgs :: a -> b -> a
+= x referArgs x _
この関数、どちらもやっていることはconst
と変わりません。dontReferArgs
はconst
をそのまま使うことでポイントフリースタイルにしていますが、referArgs
は自前で引数に言及することでconst
と同等の定義となっています。ポイントフリースタイルに変えると言うことは原則として元の関数の挙動を変えないワケですから、dontReferArgs
とreferArgs
の意味は変わらないはず、ですよね3?
ところがこれらの関数をStrict
拡張を有効にした上で定義すると、なんと挙動が異なってしまいます!
使用例:
+main :: IO ()
+= do
+ main print $ dontReferArgs "dontReferArgs" (undefined :: Int)
+ print $ referArgs "referArgs" (undefined :: Int)
実行結果(Strict拡張を有効にしなかった場合):
+> stack exec runghc const.hs
+"dontReferArgs"
+"referArgs"
実行結果(Strict拡張を有効にした場合):
+> stack exec -- runghc --ghc-arg=-XStrict const.hs
+"dontReferArgs"
+const.hs: Prelude.undefined
+CallStack (from HasCallStack):
+error, called at libraries\base\GHC\Err.hs:79:14 in base:GHC.Err
+ undefined, called at const.hs:10:34 in main:Main
はい、where
句のケースと同様、Strict
拡張を有効にした場合、例外が発生してしまいました❗️Strict
拡張を有効にした結果、意図せず例外を発生させる値(今回の場合undefined
)が評価されてしまったのです。
例外を発生させた関数はそう、ポイントフリースタイルでない、referArgs
関数の方です!なぜreferArgs
でのみ例外が発生してしまったのかというと、referArgs
がStrict
拡張を有効にしたモジュールで、引数に言及(パターンマッチ)しているからです。Strict
拡張を有効にした結果「あらゆる関数やローカル変数の定義において、パターンマッチで代入した変数の値」が評価されるとおり、referArgs
の引数x
・_
も必ず評価されるようになり、このような例外が発生したのです。たとえ使用しない変数_
でも関係ありません!
そのため、原因の本質は引数に言及(パターンマッチ)しているか否かであり、Prelude
のconst
を使用しているか否かではありません。こちら👇のように引数に言及した上でconst
を使っても、結果は同じなのです。
referArgsByConst :: a -> b -> a
+= const x y referArgsByConst x y
print $ referArgsByConst "referArgsByConst" (undefined :: Int)
一方、dontReferArgs
については、引数に言及せず、Prelude
にあるconst
をそのまま使っています。Strict
拡張はあくまでも「パターンマッチした変数」のみをWHNFまで評価するものであり、あらゆる関数が正格に呼び出されるわけではありません。なので通常のPrelude
におけるconst
と同様、dontReferArgs
も第2引数は評価しないため、undefined
を渡しても例外は起こらなかったのです。
このことは、「Strict
拡張を有効にしているモジュールの中でも、Strict
を有効にしていないモジュール(この場合はPrelude
)からimport
した関数は、引数を正格に評価しない」という忘れてはならないポイントも示しています。例えばconst
よりももっと頻繁に使われるであろう、言及する引数を一つ削除する演算子の代表、関数合成.
を使ったケースを考えてみてください。
ポイントフリースタイルに慣れた方なら、関数適用$
を次👇のように使って定義したf
を見ると、
= map (+ 3) $ filter (> 2) xs
+ f xs
+-- あるいは、`$`を使わないでこのように書いた場合も:
+= map (+ 3) (filter (> 2) xs) f xs
こちら👇のように書き換えたくなってうずうずするでしょう。
+= map (+ 3) . filter (> 2) f
しかし、Strict
を有効にしたモジュールでこのような書き換えを行うと、f
の挙動が変わってしまいます。引数.
を使って書き換える前は、引数xs
に言及していたところ.
を使って引数xs
に言及しなくなったからです。filter
もmap
もStrict
拡張を有効にしたモジュールで定義されているわけではないので、引数を正格に評価しないんですね。結果、こうした書き換えによって、Strict
拡張を有効にしていても意図せず遅延評価してしまう、というリスクがあるので、リファクタリングの際はくれぐれも気をつけてください4。ざっくりまとめると、Strict
拡張を有効にしているモジュールでは、「引数や変数を宣言することすなわちWHNFまで評価すること」、あるいは「引数や変数を宣言しなければ、評価されない」と意識しましょう。
ちなみに、referArgs
における_
のように「Strict
拡張を有効にした場合さえ、使用していない引数が評価されてしまうのは困る!」という場合は、引数名の前にチルダ~
を付けてください。
referArgs :: a -> b -> a
+~_ = x referArgs x
サンプル: 今回はGHCiですべて紹介するのでサンプルはありません。
+続いては、Strict
拡張のドキュメントでも触れられている、入れ子になったパターンマッチにおける問題を紹介します。一言で言うと、let (a, b) = ...
のような、データ構造(この場合タプルですね)の「内側」に対するパターンマッチは、Strict
拡張を有効にしていても正格に評価しないよ、という話です。
例えば、下記のコードをStrict
拡張付きで実行しても、パターンマッチしているa
・b
ともに代入した時点では正格評価されず、error "a"
・error "b"
による例外はいずれも発生しません。次のコードをGHCiで試してみてください。
> :set -XStrict
+> (a, b) = (error "a", error "b")
+-- 何も起きない
先ほどの節における「Strict
拡張を有効にしているモジュールでは、『引数や変数を宣言することすなわちWHNFまで評価すること」』、あるいは『引数や変数を宣言しなければ、評価されない』と意識しましょう」という主張を真に受けてしまうと、意図せず遅延評価させてしまい、ハマりそうです😰。⚠️繰り返しますが「内側のパターンにおける変数は正格評価されない」ということも意識してください。
一方、StrictData
や正格性フラグを用いるなどして、各要素を正格評価するよう定義した値コンストラクターでは、ちゃんと評価して例外を発生させます。
> :set -XStrict
+> data MyTuple a b = MyTuple a b deriving Show
+> let MyTuple a b = MyTuple (error "a") (error "b")
+*** Exception: b
+CallStack (from HasCallStack):
+error, called at <interactive>:10:40 in interactive:Ghci7
Strict
拡張を有効にするとStrictData
も自動的に有効になるので、👆におけるMyTuple
値コンストラクターは各要素を正格評価するようになったわけです。なのでStrict
拡張を有効にしたモジュールにおいて、なおかつそこで定義した型で完結している限りは平和でしょう。
ただし、GHCiで試す場合に特に注意していただきたいのですが、GHCiでlet
をつけないでパターンマッチした場合は正格評価されない、という点に注意してください。let
をつけないとトップレベルでの定義と見なされるからです。Strict拡張のドキュメントにも、「Top level bindings are unaffected by Strict
」とありますとおり、トップレベルでの定義は例外扱いされているのです。
> :set -XStrict
+> data MyTuple a b = MyTuple a b deriving Show
+> MyTuple a b = MyTuple (error "a") (error "b")
+-- 何も起きない
foldr
に渡す関数サンプル: stackoverflow-foldr.hs
+ここの話はちょっと難しいので、先に守るべきルールを述べておきます。
+「遅延評価する関数を受け取る前提の高階関数に、(Strict
拡張などで)引数を正格に評価するよう定義された関数を渡すのは止めましょう。」
なんだかこう書くと半ばトートロジーのようにも聞こえますが、より具体的には、例えばfoldr
に引数を正格に評価するよう定義された関数を渡すのは止めましょう、という話です。Strict
拡張を有効にした状態では、ラムダ式にも注意しないといけないもポイントです。
※あらかじめおことわり: この節のお話は、あくまでもリストに対するfoldr
の場合のお話です。他のFoldable
な型では必ずしも当てはまらないのでご注意ください。
論より証拠で、サンプルコードの中身(抜粋)とその実行結果を見てみましょう。
+-- ...
+evaluate . length $ foldr (:) [] [1 .. size]
+putStrLn "DONE: foldr 1"
+
+evaluate . length $ foldr (\x z -> x : z) [] [1 .. size]
+putStrLn "DONE: foldr 2"
+今回のサンプルコードを実行する際は、GHCのランタイムオプションを設定して、スタック領域のサイズを減らしてください。そうでなければ、処理するリストがあまり大きくないのでStrict
拡張を有効にしても問題の現象は再現されないでしょう5。こちらのStackoverflowの質問曰く、runghc
で実行する際にランタイムオプションを設定する場合は、GHCRTS
環境変数を使用するしかないそうです。
実行結果(Strict拡張を有効にしなかった場合):
+> GHCRTS=-K100k stack exec runghc -- ./stackoverflow-foldr.hs
+DONE: foldr 1
+DONE: foldr 2
実行結果(Strict拡張を有効にした場合):
+> GHCRTS=-K100k stack exec runghc -- --ghc-arg=-XStrict ./stackoverflow-foldr.hs
+DONE: foldr 1
+stackoverflow-foldr.hs: stack overflow
サンプルコードは整数のリストに対して特に何も変換せずfoldr
する(そして、length
関数でリスト全体を評価してから捨てる)だけのことを2回繰り返したコードです。最初のfoldr
はStrict
拡張があろうとなかろうと無事実行できたにもかかわらず、Strict
拡張を有効にした二つめのfoldr
は、stack overflow
というエラーを起こしてしまいました💥!
なぜこんなエラーが発生したのかを知るために、foldr
の定義を見直しましょう。こちら👇はGHC 8.10.1における、リストに対するfoldr
の定義です(コメントは省略しています)。
foldr :: (a -> b -> b) -> b -> [a] -> b
+foldr k z = go
+where
+ = z
+ go [] :ys) = y `k` go ys go (y
go
という補助関数を再帰的に呼び出すことで、第一引数として渡した関数k
を用いてリストの要素(y
)を一つずつ変換しています。呼び出す度にリストの残りの要素をチェックして、最終的に空のリストを受け取ったときはfoldr
の第二引数z
を返していますね。
このときk
が第二引数を遅延評価する関数であった場合、 — サンプルコードで言えば(:)
の場合 — 受け取ったgo ys
という式は直ちには評価されません。サンプルコードの(:)
に置き換えると、(:)
の第二引数、つまりリストの残りの要素を取り出す度にgo ys
を一回計算して、一個ずつ要素を作り出すイメージです。
一方、k
が第二引数を正格評価する関数であった場合、 — サンプルコードで言うところの、Strict
拡張を有効にした(\x z -> x : z)
の場合 — k
は受け取ったgo ys
をすぐに評価しようとします。このとき、GHCはk
やgo
に渡されている引数をスタック領域に積みます6。そうしてgo
と、go
に呼ばれたk
が次々と引数をスタック領域に積んだ結果、スタックサイズの上限に達し、スタックオーバーフローが発生してしまうのです。
これは他の多くのプログラミング言語で(末尾再帰じゃない、普通の)再帰呼び出しを行った場合とよく似た振る舞いです。間違って無限再帰呼び出しをしてしまってスタック領域があふれる、なんて経験は多くのプログラマーがお持ちでしょう。つまり単純に、Strict
拡張を有効にした場合のfoldr (\x z -> x : z) []
は、再帰呼び出しをしすぎてしまう関数となるのです。
なお、今回はlength
関数を使ってリスト全体を使用するコードにしましたが、遅延リストらしくfoldr
の結果を一部しか使わない、という場合、foldr
に渡した関数がリストを都度正格評価してしまうので、無駄な評価が占める割合はもっと増えることになります。やはりfoldr
は遅延評価を前提とした高階関数と言えるでしょう。
以上のとおり、Haskellにはfoldr
のような、遅延評価を前提とした関数がStrict
拡張より遥か昔から存在しています。それらをStrict
拡張を有効にした状態で使うと、思わぬ衝突が起きてしまうので、くれぐれも気をつけましょう。
こういう「使ってはいけない関数」を引いてしまわないための方法についても一点補足します。HLintを細かく設定したり、カスタムPrelude
を設定したりしてみるのは、一つの作戦です。なんとプロジェクト全体で、foldr
を禁止することができます(一部のモジュールでは例外的に許可する、なんてこともできます)。詳しくは「素晴らしき HLint を使いこなす」や「Prelude を カスタムPrelude で置き換える」をご覧ください。
undefined
を受け取るメソッドサンプル: storable.hs
+最後はちょっとレアケースではありますが、こちら👇のIssueで発覚した問題を解説しましょう。
+ +問題を簡単に再現するために、次のサンプルコードを用意しました。
+-- importなどは当然省略!
+data Test = Test Int Int deriving Show
+
+instance Storable Test where
+= sizeOf (1 :: Int) * 2
+ sizeOf _ = 8
+ alignment _ = error "This should not be called in this program"
+ peek = error "This should not be called in this program"
+ poke
+= alloca $ \(_ :: Ptr Test) -> putStrLn "This won't be printed when Strict is enabled" main
はい、適当な型を定義してStorable
のインスタンスにして、それに対してalloca
を呼ぶだけのコードです。インスタンス定義をはじめかなり手抜きな感じになっちゃってますが、まぁ今回の問題を再現するのにはこれで十分なので、ご了承ください🙏。
このコード、残念ながらStrict
拡張を有効にした状態で実行すると、undefined
による例外が発生してしまいます💥。
> stack exec -- runghc --ghc-arg=-XStrict storable.hs
+storable.hs: Prelude.undefined
+CallStack (from HasCallStack):
+error, called at libraries\base\GHC\Err.hs:79:14 in base:GHC.Err
+ undefined, called at libraries\base\Foreign\Marshal\Alloc.hs:117:31 in base:Foreign.Marshal.Alloc
こちらはStrict
を有効にしなかった場合。やはり例外は起きてませんね😌。
> stack exec -- runghc storable.hs
+This won't be printed when Strict is enabled
さてこの、Strict
拡張を有効にした場合に発生した、undefined
による例外はどこからやってきたのでしょう?上記のコードにはいくつかerror
関数を使用している箇所がありますが、発生した例外はあくまでもundefined
です。見た限り上記のコードそのものから発生した例外ではなさそうですね…🤔。
その答えはなんと、main
関数で呼んでいるalloca
の定義にありました!
alloca :: forall a b . Storable a => (Ptr a -> IO b) -> IO b
+=
+ alloca undefined :: a)) (alignment (undefined :: a)) allocaBytesAligned (sizeOf (
確かに、sizeOf
メソッドやalignment
メソッドにundefined
を渡しています。これらはいずれもStorable
型クラスのメソッドなので、上記のTest
型でももちろん実装しています。そう、実はこのsizeOf
メソッドとalignment
メソッドの実装で、下👇のように引数_
を宣言しているのが問題なのです!
instance Storable Test where
+= sizeOf (1 :: Int) * 2
+ sizeOf _ = 8
+ alignment _ -- ...
「Case 2: ポイントフリースタイルかどうかで変わる!」の節で、「Strict
拡張を有効にしているモジュールでは、『引数や変数を宣言することすなわちWHNFまで評価すること」』、あるいは『引数や変数を宣言しなければ、評価されない』」と述べたことを再び思い出してください。こちらのsizeOf
・alignment
の定義でも同様に、引数_
を宣言しているため、引数を必ずWHNFまで評価することになっています。結果、alloca
関数がそれぞれを呼ぶ際undefined
を渡しているため、undefined
を評価してしまい、undefined
による例外が発生してしまうのです💥。
なぜこのように、alloca
関数ではsizeOf
やalignment
にundefined
をわざわざ渡しているのでしょう?それは、これらのメソッドがそもそもundefined
を渡して使うことを前提に設計されているからです。sizeOf
・alignment
はともにStorable a => a -> Int
という型の関数なので、第一引数にStorable
のインスタンスである型a
の値を受け取るのですが、このとき渡されるa
型の値は、使わないこととなっています。それぞれのメソッドの説明にも「The value of the argument is not used.」と書かれていますね。これは、sizeOf
もalignment
も、型毎に一意な値として定まる(引数の値によってsizeOf
やalignment
の結果が変わることがない)ので、第一引数のa
は、単に「この型のsizeOf
を呼んでくださいね」という型の情報を渡すためのものでしかないからです。だから値には関心がないのでundefined
を渡しているわけです。そもそも、alloca
関数のように引数としてStorable a => a
型の値をとらない関数では、a
型の値を用意することができませんし。
現代では通常、このように「値に関心がなく、何の型であるかという情報だけを受け取りたい」という場合は、Proxy
型を使うのが一般的です。Storable
は恐らくProxy
が発明される前に生まれたため、undefined
を渡すことになってしまっているのでしょう。なので、Storable
型クラスのインスタンスを自前で定義したりしない限り、こうしたケースに出遭うことはまれだと思います。ただ、それでもProxy
をimport
するのを面倒くさがってundefined
を代わりに渡す、なんてケースはありえるので、Proxy
を使って定義した型クラスでも同じ問題にハマることはあるかも知れません…。
⚠️結論として、Storable
型クラスや、Proxy
を受け取るメソッドを持つ型クラスのインスタンスを、Strict
拡張を有効にした状態で定義する場合は、Proxy
にあたる引数を評価しないよう、~_
などを使って定義しましょう。
Strict
は使う?使わない?さて、ここまでStrict
拡張を有効にすることによって犯しうる、数々のミスを紹介してきました。ここまで書いた個人的な印象としては、「敢えて有効にする必要はないんじゃないか」といったところです(まぁ、悪いところばかり調べた結果のため、とてもフェアな視点での判断とは言えないのですが…)。foldr
の例でも触れたとおり、Haskellには遅延評価を前提とした、遅延評価を存分に活かした機能が溢れています。当然それらはStrict
拡張ができるよりはるか昔からあり、Strict
拡張のことなど一切考えないで作られたものです。動的型付け言語に後から静的型検査を導入するのが大変なように、相対する機能を後付けすると衝突が起こるのは仕方のないことですが、ことStrict
拡張については想像以上に大きな衝突のようです😞。
それでも使いたいという方に、今回の記事が助けになれば幸いです💪それではStrict
な方もNoStrict
な方もHappy Haskell Hacking!!
例えばfumievalさんによるこの記事より: 「もっとも、日常ではここまで気にしなければいけない場面は少ないので、ほとんどの場合は気にせず感嘆符をつけて大丈夫だろう。GHC 8.0からは、全フィールドをデフォルトで正格にするStrictData
という拡張が入るため、こちらを使おう」↩︎
BangPatterns
言語拡張を有効にした上で上記のように書き換えてみると、Strict
拡張の有無に関わらずエラーが発生します。試してみましょう。↩︎
実際のところ今回紹介するケース以外にも、ポイントフリースタイルにするかしないかで実行効率などが変わる場合があります。例えば、Evaluation of function calls in Haskellをご覧ください。↩︎
もっとも、この例では引数はリストでしょうから、WHNFまでのみ正格評価するメリットは少なそうですが。↩︎
大きなリストにすると、今度はエラーが発生するまでに時間がかかってしまうので…。ちなみに、このようにスタック領域を小さくすることでスペースリークを検出する手法は、ndmitchell/spaceleak: Notes on space leaksでも紹介されています。↩︎
GHCがどのように評価し、スタック領域を消費するかはGHC illustratedや、その参考文献をご覧ください。↩︎
Haskell Day 2021を開催します!
+ +こんにちはkakkun61こと岡本和樹です。
+この記事ではHaskell Day 2021の紹介と開催の経緯などを記載します。
+ +Haskell Dayは日本語で開催されるHaskellに関するイベントとしては最多の参加者を誇るイベントです。これまで2012・2016・2018・2019と開催してきました。新型コロナウイルスの影響により、残念ながら2020は開催しませんでしたが、2021はオンラインイベントとして開催します。
+このようなオンラインイベントの開催は未経験だったため、さまざまなイベント形式を検討した結果、今回は事前録画動画の予約公開という形式を採用しました。生放送ももちろん検討しましたが、ノウハウ不足の中で一発勝負という生放送はリスクが大きいという判断をしました。録画公開における臨場感の不足をおぎなうことを期待し、YouTubeのプレミア公開を使用しリアルタイムチャットによる発表者と視聴者・視聴者同士の交流をできるように予定しています。
+現在発表者募集中です!
+今回はオンライン開催ということで、お手数ですが発表者にもオンサイトのイベントと異なった準備をお願いすることになります。運営としてできるかぎりのサポートをしますので安心して応募いただければと思います。
+Connpassにて参加登録の受け付けもしていますので視聴者の方も登録をお願いします。
+その他のくわしい情報はHaskell Day 2021イベントページをご覧ください。
+みなさまのご応募をお待ちしています。またお体にお気をつけください。
+この記事はググって解決しづらかったこと Advent Calendar 2021の25日目の記事です。Haskell-jp WikiのHaskellの歩き方というページにもほぼ同じことを書きましたが、今回はよい機会なので実例を加えつつ詳しく紹介させてください。
+よく知られているとおり、Haskellには二項演算子をプログラマーがかなり自由に定義できるという、とても変わった特徴があります。他のプログラミング言語でも使う標準的なもの(例: +
, *
, &&
など)を名前空間を絞って置き換えるほか、例えばかのlensパッケージのように、ライブラリーの作者があたかも新しい構文を作り上げるかのごとく独自の二項演算子を提供することができます。
これは面白い機能ではあるものの、しばしば混乱を招く機能でもあります。後述するその他の記号との区別がつきにくいですし、一般的な検索エンジンで検索することさえままなりません。Googleはプログラミングでよく使われる記号による検索をサポートはしているものの、Haskellでしか見ないような記号の組み合わせは到底無理でしょう。
+そんな背景もあり、Haskellを使う人はしばしばHoogleなどの、関数名で検索できる検索エンジンを使用することになります。こちらは二項演算子の名前での検索もサポートしています。
+例えばlensパッケージでおなじみの^.
で検索すると次のような結果になりました:
lensパッケージ以外でも、同様の^.
が定義されているのが分かりますね。lensパッケージは依存関係がとても大きい一方、^.
などの定義は十分単純でコピペしてもいいくらい小さいので、このようにいくつものパッケージで定義されています。
また、特によく使われる二項演算子はFPCompleteのウェブサイトでもまとめられています:
+ +Haskell、というかそのデファクトスタンダードな処理系であるGHCでは、言語拡張という形で長年新しい構文が提案されています1。その中には、当然これまでにない方法で記号を使っているものもあります。そうした記号はプログラマーが定義した関数ではないので、前述のHoogleなどを使った方法が通用しません。そこで、当ブログにも何度も寄稿いただいた@takenobu_hsさんが、言語拡張によるものも含めた、Haskellの構文における記号の一覧を作ってくださいました!
+takenobu-hs/haskell-symbol-search-cheatsheet
+実は日本語版もQiitaにあるのですが、上記のGitHub版の方が更新されているようです。そこで、今回はおまけとして、GitHub版の方にも載っている、GHCに最近(バージョン9.2.1以降に)追加された、新しいピリオド .
の使い方を紹介しましょう。
従来、Haskellでピリオドといえば関数合成を表す二項演算子でした:
+> f x = x + 1
+ ghci> g x = x * 3
+ ghci> h = g . f
+ ghci> h 2
+ ghci9 -- 2 に + 1 して * 3 した結果
数学における関数合成の記号「g ∘ f」に似せてピリオドを採用したのでしょう。しかし、世は今まさに大「ピリオドといえばフィールド2へのアクセス演算子じゃろがい」時代です。それでなくてもHaskellのレコード型は扱いにくいと言われているのに、フィールドへのアクセスまで変なやり方でした3:
+data SomeRecord =
+SomeRecord { field1 :: String, field2 :: Int }
+
+= SomeRecord "value1" 2
+ someRecord
+> field1 someRecord
+ ghci"value1"
+
+> field2 someRecord
+ ghci2
そこで、GHC 9.2からはOverloadedRecordDot
という言語拡張が導入され、これを有効にしたファイルではおなじみの言語のようにピリオドでレコードのフィールドにアクセスできるようになりました:
(以下はGHCiで使用した例です)
+> :set -XOverloadedRecordDot
+ ghci
+> someRecord.field1
+ ghci"value1"
+
+> someRecord.field2
+ ghci2
+
+-- ⚠️ピリオドの前後に空白を入れると関数合成として解釈されてしまう!
+> someRecord . field2
+ ghci
+<interactive>:5:1: error:
+? Couldn't match expected type ‘Int -> c’
+ type ‘SomeRecord’
+ with actual ? In the first argument of ‘(.)’, namely ‘someRecord’
+ In the expression: someRecord . field2
+ In an equation for ‘it’: it = someRecord . field2
+ ? Relevant bindings include
+ it :: SomeRecord -> c (bound at <interactive>:5:1)
OverloadedRecordDot
についてのより詳しい解説は、Haskell Day 2021における、fumievalさんの発表をご覧ください。
OverloadedRecordDot
拡張について勉強しましょう。🎁それでは2022年もHappy Haskell Hacking!!🎅
+余談: ghc-proposalsに送られたPull requestを見ると、今どのような提案が議論されているか分かります。↩︎
他のプログラミング言語では「プロパティー」と呼ばれることも多いですが、ここではHaskellのレコード型における用語に合わせました。↩︎
個人的にはゲッターが関数になるのはとても直感的な気がして割と好きでしたが、確かにデメリットもとても多い仕様でした。セッターは単純な関数になってないですしね。↩︎
この記事はHaskell Advent Calendar 2021の25日目の記事です。
+Haskellのよく言われる問題点の一つとして、文字列型が下記のようによく使われるものだけで5種類もある、という点があります:
+String
Text
Text
ByteString
ByteString
(上記の頻繁に使われるもの以外にも、もっとあります)
+それぞれ確かに使いどころが違うので、アプリケーションで使用する場合は場面に応じて使い分ければいいのですが、文字列を使ったライブラリーを開発する場合はなかなか悩ましいものがあります。内部で依存しているライブラリーが使用しているものがあれば、それをそのまま使うのが簡単で確実ですが、そうでない場合も多いでしょう。そこで本稿では文字列型を抽象化して扱いたい場合の手段として、mono-traversalbeパッケージを検討したいと思います。
+mono-traversableパッケージは、名前のとおりMonoTraversable
やMonoFoldable
、MonoFunctor
といったおなじみの型クラスの名前にMono
という接頭辞を付けた型クラスによって、多様なコンテナ型を抽象化してくれます。これらの型クラスはすべて、ByteString
やText
のような、「要素として持てる値の型が1種類だけ」の型も対象にしているのが特徴です。Type Familyを応用し、次のように型毎に要素の型を固定することで、そうした特徴を実現しています:
type family Element mono
+
+type instance Element ByteString = Word8
+type instance Element Text = Char
+type instance Element [a] = a
+
+-- ...
+
+class MonoFunctor mono where
+-- ...
+ omap :: (Element mono -> Element mono) -> mono -> mono
+
+instance MonoFunctor ByteString where
+= ByteString.map
+ omap
+instance MonoFunctor Text where
+= Text.map
+ omap
+instance MonoFunctor [a] where
+= map omap
※mono-traversableパッケージのソースから引用して少し改変しました。
+さらに、これまで紹介したMonoTraversable
やMonoFoldable
、MonoFunctor
に加えて、SemiSequence
やIsSequence
という型クラスで分解や構築に関わる操作(例えばcons
やbreak
)などの他、(今回は取り上げませんが)SetContainer
などの型クラスでMap
やSet
、IntMap
などの型まで抽象化してくれます。
そこで次の節では、このmono-traversableパッケージにおける型クラスを中心に、Data.Text
モジュールやData.ByteString
モジュールにおける各関数が、どの型クラスに対するどの関数に対応するのか、まとめた表を作ってみました。
Monoid
、Semigroup
などのメソッドも調査対象に含めましたString
についてはbaseパッケージにある関数のみを対象にしていますが、Data.List
モジュールのドキュメントと自分の記憶を頼りに埋めているので間違いがあるかも知れませんText
・ByteString
についてはStrictなバージョンのドキュメントのみ参照しています。Lazyな方になかったらごめんなさい!Textual
型クラスについては、ByteString
がインスタンスになっていないのでご注意くださいIO
が絡むものText |
+ByteString |
+String ([Char] ) |
+型クラス / 関数 | +
---|---|---|---|
all |
+all |
+all |
+MonoFoldable / oall |
+
any |
+any |
+any |
+MonoFoldable / oany |
+
append |
+append |
+++ |
+Semigroup / <> |
+
N/A | +breakByte |
+N/A | +N/A | +
N/A | +breakEnd |
+N/A | +N/A | +
breakOnAll |
+N/A | +N/A | +N/A | +
breakOnEnd |
+N/A | +N/A | +N/A | +
breakOn |
+breakSubstring |
+N/A | +N/A | +
break |
+break |
+break |
+IsSequence / break |
+
center |
+N/A | +N/A | +N/A | +
chunksOf |
+N/A | +N/A | +N/A | +
commonPrefixes |
+N/A | +N/A | +N/A | +
compareLength |
+N/A | +N/A | +N/A | +
concatMap |
+N/A | +concatMap |
+MonoFoldable / ofoldMap |
+
concat |
+concat |
+concat |
+MonoFoldable / ofold |
+
cons |
+cons |
+N/A | +SemiSequence / cons |
+
copy |
+copy |
+N/A | +N/A | +
count |
+count |
+N/A | +N/A | +
dropAround |
+N/A | +N/A | +N/A | +
dropEnd |
+N/A | +N/A | +IsSequence / dropEnd |
+
dropWhileEnd |
+dropWhileEnd |
+dropWhileEnd |
+N/A | +
dropWhile |
+dropWhile |
+dropWhile |
+IsSequence / dropWhile |
+
drop |
+drop |
+drop |
+IsSequence / drop |
+
N/A | +elemIndexEnd |
+N/A | +N/A | +
N/A | +elemIndex |
+elemIndex |
+N/A | +
N/A | +elemIndices |
+elemIndices |
+N/A | +
N/A | +elem |
+elem |
+MonoFoldable / oelem |
+
empty |
+empty |
+"" |
+Monoid / mempty |
+
filter |
+filter |
+filter |
+IsSequence / filter |
+
N/A | +findIndexEnd |
+N/A | +N/A | +
findIndex |
+findIndex |
+findIndex |
+N/A | +
N/A | +findIndices |
+findIndices |
+N/A | +
N/A | +findSubstring |
+N/A | +N/A | +
N/A | +findSubstrings |
+N/A | +N/A | +
find |
+find |
+find |
+SemiSequence / find |
+
foldl' |
+foldl' |
+foldl' |
+MonoFoldable / ofoldl' |
+
foldl1' |
+foldl1' |
+foldl1' |
+MonoFoldable / ofoldl1Ex' |
+
foldl1 |
+foldl1 |
+foldl1 |
+N/A | +
foldl |
+foldl |
+foldl |
+N/A | +
N/A | +foldr' |
+N/A | +N/A | +
N/A | +foldr1' |
+N/A | +N/A | +
foldr1 |
+foldr1 |
+foldr1 |
+MonoFoldable / ofoldr1Ex |
+
foldr |
+foldr |
+foldr |
+MonoFoldable / ofoldr |
+
groupBy |
+groupBy |
+groupBy |
+IsSequence / groupBy |
+
group |
+group |
+group |
+IsSequence / group |
+
head |
+head |
+head |
+MonoFoldable / headEx |
+
index |
+index |
+index |
+IsSequence / indexEx |
+
init |
+init |
+init |
+IsSequence / initEx |
+
inits |
+inits |
+inits |
+N/A | +
intercalate |
+intercalate |
+intercalate |
+MonoFoldable / ointercalate |
+
intersperse |
+intersperse |
+intersperse |
+SemiSequence / intersperse |
+
isInfixOf |
+isInfixOf |
+isInfixOf |
+IsSequence / isInfixOf |
+
isPrefixOf |
+isPrefixOf |
+isPrefixOf |
+IsSequence / isPrefixOf |
+
isSuffixOf |
+isSuffixOf |
+isSuffixOf |
+IsSequence / isSuffixOf |
+
justifyLeft |
+N/A | +N/A | +N/A | +
justifyRight |
+N/A | +N/A | +N/A | +
last |
+last |
+last |
+MonoFoldable / lastEx |
+
length |
+length |
+length |
+MonoFoldable / olength |
+
lines |
+N/A | +lines |
+Textual / lines |
+
mapAccumL |
+mapAccumL |
+mapAccumL |
+N/A | +
mapAccumR |
+mapAccumR |
+mapAccumR |
+N/A | +
map |
+map |
+map |
+MonoFunctor / omap |
+
maximum |
+maximum |
+maximum |
+MonoFoldable / maximumEx |
+
minimum |
+minimum |
+minimum |
+MonoFoldable / minimumEx |
+
N/A | +notElem |
+notElem |
+MonoFoldable / onotElem |
+
null |
+null |
+null |
+MonoFoldable / onull |
+
pack |
+pack |
+id |
+IsString / fromString |
+
partition |
+partition |
+partition |
+IsSequence / partition |
+
replace |
+N/A | +N/A | +IsSequence / replaceSeq |
+
replicate |
+replicate |
+replicate |
+IsSequence / replicate |
+
reverse |
+reverse |
+reverse |
+SemiSequence / reverse |
+
scanl1 |
+scanl1 |
+scanl1 |
+N/A | +
scanl |
+scanl |
+scanl |
+N/A | +
scanr1 |
+scanr1 |
+scanr1 |
+N/A | +
scanr |
+scanr |
+scanr |
+N/A | +
singleton |
+singleton |
+singleton |
+MonoPointed / opoint |
+
snoc |
+snoc |
+snoc |
+SemiSequence / snoc |
+
N/A | +sort |
+sort |
+SemiSequence / sort |
+
N/A | +spanEnd |
+N/A | +N/A | +
span |
+span |
+span |
+SemiSequence / span |
+
splitAt |
+splitAt |
+splitAt |
+IsSequence / splitAt |
+
splitOn |
+N/A | +splitOn |
+IsSequence / splitSeq |
+
N/A | +splitWith |
+N/A | +IsSequence / splitElem |
+
split |
+splitWith |
+N/A | +IsSequence / splitWhen |
+
stripEnd |
+N/A | +N/A | +N/A |
+
stripPrefix |
+stripPrefix |
+stripPrefix |
+IsSequence / stripPrefix |
+
stripStart |
+N/A | +N/A | +N/A | +
stripSuffix |
+stripSuffix |
+N/A | +IsSequence / stripSuffix |
+
strip |
+N/A | +N/A | +N/A | +
tail |
+tail |
+tail |
+IsSequence / tail |
+
tails |
+tails |
+tails |
+N/A | +
takeEnd |
+N/A | +N/A | +N/A | +
takeWhileEnd |
+takeWhileEnd |
+N/A | +N/A | +
takeWhile |
+takeWhile |
+takeWhile |
+IsSequence / takeWhile |
+
take |
+take |
+take |
+IsSequence / take |
+
toCaseFold |
+N/A | +N/A | +Textual / toCaseFold |
+
toLower |
+N/A | +N/A | +Textual / toLower |
+
toTitle |
+N/A | +N/A | +N/A | +
toUpper |
+N/A | +N/A | +Textual / toUpper |
+
transpose |
+transpose |
+N/A | +N/A | +
uncons |
+uncons |
+N/A | +IsSequence / uncons |
+
unfoldrN |
+unfoldrN |
+N/A | +N/A | +
unfoldr |
+unfoldr |
+unfoldr |
+N/A | +
unlines |
+N/A | +unlines |
+Textual / unlines |
+
unpack |
+unpack |
+id |
+MonoFoldable / otoList |
+
unsnoc |
+unsnoc |
+N/A |
+SemiSequence / unsnoc |
+
unwords |
+N/A | +unwords |
+Textual / unwords |
+
words |
+N/A | +words |
+Textual / words |
+
zipWith |
+zipWith |
+zipWith |
+MonoZip / ozipWith |
+
zip |
+zip |
+zip |
+MonoZip / ozip |
+
以上です。残念ながら万能とはいかないようで、いくつか「N/A」、すなわち対応するものがない関数もありますが、他の関数の組み合わせで実装できるものもあるでしょう。
+MonoTraversable
などに限らず、型クラスを使って関数を多相化したとき全般に言えることですが、コンパイル時にインスタンスの解決が行えなかった場合、直接対象の型の相当する関数を呼ぶより少し遅くなってしまう場合があります(参考)。
また、それに限らず、各型クラスのメソッドでない関数は、各型の相当する関数でオーバーライドできないため、効率の悪い処理になってしまう恐れがあります。例えば、ointercalate
関数の実装を見ると、Text
やByteString
などについてはRULES
プラグマで最適な実装を設定しているようですが、それ以外の型については一旦リストに変換してから結合する、という効率の悪そうな処理をしています。
String
から相互変換できる型を抽象化する最後に、最近私が作った(まだリリースしてない)ライブラリーにおいて、MonoFoldable
とIsString
を使うことで、Text
とString
両方をサポートした関数を紹介しておきます。ただ、時間とやる気パワーが残り少なくなってしまったので、該当の箇所だけこちらからコピペして、説明は簡単にしておきます:
stringVal :: (IsString a, MT.MonoFoldable a, MT.Element a ~ Char) => CodecEnvVal a
+= valByFunction CodecEnvValByFunction
+ stringVal = MT.otoList
+ { encode = Right . fromString
+ , decode }
CodecEnvVal a
型は、a
型をString
型と相互変換するための情報を含んだ型です。stringVal
の場合、名前のとおり文字列っぽい型とString
との相互変換ができなければなりません。もちろん単純にString
型だけをサポートしてText
用には別途CodecEnvVal Text
を作ってもいいのですが、一つのCodecEnvVal a
だけで扱えた方が楽でしょうし、今回はMonoFoldable
のotoList
とIsString
のfromString
を使って両方をサポートすることにしました。なお、これではByteString
がサポートできませんが、ここで相互変換するString
は、要件上人間が読み書きするファイルにおける文字列を想定しているので、ByteString
はバイナリーデータにだけ使うべきだ、という立場から敢えてサポートしていません。
mono-traversableパッケージをうまく使えば、自前で専用の型クラスを作らなくてもString
・Text
・ByteString
などを一挙にサポートする関数が書けるかも知れません!
それでは2022年はmono-traversableでHappy Haskell String Programming!🚝
+誠に残念ながら、一般社団法人としての日本Haskellユーザーグループ管理委員会(通称Haskell-jp Admins)は、去る2022年4月16日を以て解散しました。先日、解散後に必要な処理のほぼすべてが完了しましたので、遅くなってしまいましたが、改めてこちらで報告致します。
+ +解散の背景を一言で申しますと、「お金がかかりすぎる上に、士気も下がってしまったから」です。設立前に調査した際、法人住民税のルールを誤解してしまったことで想定より費用がかさんだことや、当初活動の一環として掲げていた大規模なオフラインイベントが、現状のコロナ禍において難しく、その上、代表を含む社員自身の開催するモチベーションが下がってしまっていることを鑑みて、先般行われた第1期の社員総会で解散する事を合意しました。
+「一般社団法人」という法人格を失ってしまったので、法律上必要な場面において「日本Haskellユーザーグループ管理委員会」という名前を使用することはできなくなってしまいました。それでもできる範囲内で、任意団体として今までどおりの活動を継続したいと思います。
+具体的には、
+⚠️一般社団法人としての日本Haskellユーザーグループ管理委員会は、2022年4月16日を以て解散しました。今後は任意団体として活動を続けます。詳細は一般社団法人日本Haskellユーザーグループ管理委員会 解散のお知らせをご覧ください。
+以下では、記録のために設立当時の記事をほぼそのまま残しています。
+去る2021年2月9日、任意団体であり明確な会員資格を持たない、日本Haskellユーザーグループ(Haskell-jp)における共有財産やコミュニケーションの場の管理・運営を担う法人として、一般社団法人日本Haskellユーザーグループ管理委員会(通称 Haskell-jp Admins。法人番号 5020005014971)を設立しました。法人格を持つことを活かして、Haskell-jp Adminsは次の事業に取り組みます。
+そもそもの設立の動機は、山下さん(@nobsun)の好意によって個人名義で保有していたhaskell.jp ドメインを共同で管理出来るようにするためでした。ドメインを団体として保有するには、法人格と、法人名義の銀行口座が必要なのです。これ以外にも、Haskell-jpとして共有する価値のあるアカウントを管理する際の名義として、随時「日本Haskellユーザーグループ管理委員会」を使用します。
+Haskell-jp Adminsが出来たからといって、Haskell-jpのあり方が大きく変わることはありません。今後もSlackで質問したり議論したりブログ記事を書いたりしましょう!haskell.jpというドメインを活かし、「公式面して」自由に活動する方をいつでも待っています!
+我々Haskell-jp Adminsは、そうした活動をバックアップするために種々の問題に取り組んでいきます。
+2022/09/18 編集: 法人格を廃止するとともに、契約したバーチャルオフィスの規約に従い、こちらに記載していた住所も削除しました。
+こちらに一部個人情報を削除した上で掲載しています。
+ +Haskellは、関数型プログラミングの強力なサポートと、厳密かつ柔軟な静的型付けを特徴としたプログラミング言語です。
+そうした特徴によってHaskellは、バグが少なく、堅牢でメンテナビリティーの高いソフトウェア作りをサポートします。
加えて、下記のような際立った機能を備えています。
+IO
をはじめとする状態系モナド)do
記法)日本Haskellユーザーグループは、日本におけるプログラミング言語Haskellの普及活動と、Haskellを利用する人々のサポートを行うグループです。
+日本における代表的なHaskellユーザーグループとなることを目指します。
+「日本Haskellユーザーグループ」ではやや長いので、「Haskell-jp」という愛称を設定しました。
現在は主に次のような活動を行っています。
+Haskell-jp Adminsについてはこちらの設立のお知らせをご覧ください。
+先日のHaskell-jp Adminsと同様に事務的な連絡で恐縮ですが、当ブログやHaskell-jpのSlack Workspace、GitHubのOrganizationなどにおけるコミュニケーションに適用される、「相互を尊重したコミュニケーションのためのガイドライン」を制定致しました。
+Haskell-jp 相互を尊重したコミュニケーションのためのガイドライン
+こちらはHaskell FoundationにおけるGuidelines for Respectful Communication (GRC)を日本語に翻訳し、運用主体などをHaskell-jpにおける実態に合わせて書き換えたものです。いわゆる「行動規範(Code Of Conduct。しばしば「COC」と略されます)」と同じ役割を果たすものですが、行動規範と異なり、禁止事項よりも推奨事項を数多く挙げているのが特徴です。このGRCを翻訳する前に、COCを提案した際の議論においてGRCのこうした特徴が好まれ、採用に至りました。
+このGRCは、今後Haskell-jpのSlack WorkspaceやHaskell-jpのGitHubにおけるOrganizationが管理するリポジトリー、それからHaskell-jpとして開催するイベントなど、様々な場面で適用されます。参加されるみなさんはご理解の上、快適なコミュニティー活動をお楽しみください。
+加えて、もちろん今秋開催予定のHaskell Day 2021においても、こちらのGRCを採用します。参加者、発表者、運営者の方々はご理解とご協力をよろしくお願いします。
+このほかに、Haskell-jp Wikiにもリンク集があります!
+この一覧への登録方法は下記をご覧ください。
+登録条件: 広い意味でプログラミング言語Haskellに関連したことが書いてあるWebサイトであること。
+TODO: 承認を自動化するツールを作る。
+対象のWebサイトを承認する場合は、承認する旨を伝えるとともに、下記のスニペットをコピペして、(siteId)
と書かれた箇所を「追加するWebサイトのURLからスキーム(http://
やhttps://
の部分)を取り除いたもの」で置き換え、対象のWebサイトに貼り付けてもらうようIssueのコメントで依頼しましょう。
+html
html
+
+
続いて、下記のmarkdownのスニペットにおける(siteId)
の部分を、先ほどの「追加するWebサイトのURLからスキームを取り除いたもの」で置き換え、このページを編集し、追加しましょう。
<li id="(siteId)" class="hash-target"><a href="(WebサイトのURL)">(追加するWebサイトの名前)</a>( by 運営者の氏名があれば)</li>
- - -# Invalid characterと言われたらchcp 65001しよう - -恐らく一番高確率で遭遇する & 知らないと回避できないのがこれ。 -あ、ほらまたhakyllでビルドしたら起きた! - -``` -> stack exec -- site rebuild -... - [ERROR] preprocessed-site\posts/2017/01-first.md: hGetContents: invalid argument (invalid byte sequence) -``` - -GHCがファイルを読み書きする時に使う[`Handle`](https://www.stackage.org/haddock/lts-10.0/base-4.10.1.0/System-IO.html#t:Handle)というオブジェクトには、文字コードの情報が含まれています。 - -これはRubyの[`IO`](https://docs.ruby-lang.org/ja/latest/class/IO.html)やPerlのファイルハンドラーにあるような仕組みと大体似ていて、`Handle`といったデータの「入り口」を表すオブジェクトに文字コードを紐付けることで、外から入ってくる文字列の文字コードを確実に内部の統一された文字コードに変換してくれます。 -Haskellの`Char`型の場合はUTF-32(この場合その言い方でよかったっけ?)のはずです。 - -この`Handle`に紐付ける文字コード、当然のごとくデフォルトではOSのロケール設定に従って設定されるようになってまして、日本語版のWindowsではそう、Windows-31J(またの名をCP932)ですね。 -でも今はもうすぐ2018年。あなたが「メモ帳」でプログラムを書く人でもない限り、新しく作るファイルの大半はUTF-8でしょう。 -UTF-8とWindows-31Jは全然違う体系の文字コードなので、UTF-8なファイルをWindows-31Jのファイルとして読もうとしてもうまくいかないわけです。 -冒頭にあげた`invalid byte sequence`というエラーはまさにそうした場合に起こるエラーです。 -ファイルの読み書きだけでなく標準入出力でもしばしば発生するので覚えておいてください。 - -## 対策 - -### ユーザーとして出くわした場合 - -多くの場合、このエラーは以下のコマンドをあらかじめ実行しておけば回避できます。 - -``` -> chcp 65001 -> stack exec -- site rebuild -... 動くはず! -``` - -これは、現在開いているコマンドプロンプトで一時的に文字コードを切り替えるコマンドです。 -`65001`という数字がUTF-8を指しているようです。 -もとに戻したい場合は`chcp 932`と実行しましょう。 - -``` -> chcp 932 -``` - -どうやら「CP932」の「932」はここで出てくる「932」と同じものを指しているようですね! - -どういう仕様なのか分かりませんが、このコマンド、MSYS2のbashでも使用できます。 -ただし`chcp`コマンドは`C:\Windows\System32\`という、MSYS2ユーザーにとってはあまり`PATH`に入れたくない場所に入っています。 -このディレクトリーには、`find.exe`など、Unixな方が好んで使うコマンドと同じ名前の非互換なコマンドがゴロゴロ転がっているのです! - -なので私はMSYS2を使う時は`C:\Windows\System32\`は`PATH`から抜いています。 -私と同じような方は下記のようにフルパスで実行しましょう。 - -``` -/c/Windows/System32/chcp.com 932 -``` - -### それでもダメな場合、あるいはライブラリーなどの開発者として出くわした場合 - -残念ながら、`chcp 65001`してもこのエラーが消えないことはあります[^eta-20127]。 -私の推測なんですが、どうも`chcp 65001`は`chcp 65001`したコマンドプロンプト(とかbash)の孫プロセス(つまり、あなたが入力したコマンドの子プロセス)には届かないことがあるようです。 - -[^eta-20127]: 敢えて脚注に書きますが、[Eta](http://eta-lang.org/)のコンパイラーをビルドしている時(のはず)、`chcp 65001`でもダメで`chcp 20127`ならうまくいったことがあります。 -`chcp 20127`はUS-ASCIIに切り替えるためのコマンドですが、やっぱりEtaの開発者の手元(?)ではそうなっているからなのでしょうか...? - -そんなときは、実際にエラーが起きているコマンドの開発元にバグ報告するか、自分で直してみましょう。 -バグ報告する場合は、「`chcp 932`してから実行してみて」とお願いすると、バグ報告を受けた開発者も再現しやすくて助かるかも知れません(残念ながら私はやったことがありません)。 -自分で直す場合、いろいろ方法はありますが、対象の`Handle`オブジェクトの文字コードを変えることで対処するのが、一番直接的で確実でしょう。 - -この問題は`Handle`に設定された文字コードと実際にやりとりされる文字列の文字コードに食い違いが発生しているため起こるものなのですから、適切な文字コードに変えてしまえばいいのです。 -状況にもよりますがエラーが起きた`Handle`が普通のUTF-8なファイルを読み書きするものである場合、下記のようにすれば、問題は回避できるはずです。 - -```haskell -import System.IO (hSetEncoding) -import GHC.IO.Encoding (utf8) - -hSetEncoding handle utf8 -``` - -それから、[実際に私がhaddockのバグを直した時](https://github.com/haskell/haddock/pull/566)を例に標準出力(または標準エラー出力)でこのエラーが発生した時の対応も紹介しておきます。 -コードだけ貼り付けると、下記のようにすれば少なくともエラーが起こらないようにすることはできます([このコミット](https://github.com/haskell/haddock/pull/566/commits/855118ee45e323fd9b2ee32103c7ba3eb1fbe4f2)とほぼ同じ内容です)。 - -```haskell -{-# LANGUAGE CPP #-} - -import System.IO (hSetEncoding, stdout) - -#if defined(mingw32_HOST_OS) -import GHC.IO.Encoding.CodePage (mkLocaleEncoding) -import GHC.IO.Encoding.Failure (CodingFailureMode(TransliterateCodingFailure)) -#endif - -... - -#if defined(mingw32_HOST_OS) - liftIO $ hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure -#endif -``` - -Windowsでしか使用できないモジュールを`import`している関係上、CPPのマクロが混ざって読みにくいですが、重要な部分だけ切り出すと、 - -``` -hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure -``` - -とすればよいのです。 - -一つ一つ解説しましょう。 -まず`hSetEncoding`は先ほども触れたとおり指定した`Handle`の文字コードを変更する関数です。 -そして`stdout`は名前の通り標準出力を表す`Handle`です。 -最後の`mkLocaleEncoding TransliterateCodingFailure`ですが、これはWindowsで設定された文字コード(`chcp`された文字コードと同じ)を作って、「もし(Unicodeから、あるいはUnicodeに)変換できない文字があった場合、エラーにせず、それっぽい文字に変換する」という設定で返す、という意味です。 - -結果、`chcp 932`な状態でGHCのエラーメッセージにも使われる - -``` -↓この文字 -• No instance for (Transformation Nagisa CardCommune_Mepple) -↑ -``` - -が、 - -``` -? No instance for (Transformation Nagisa CardCommune_Mepple) -``` - -のように、クエスチョンマークに変換されるようになります。そう、日本語のWindowsでGHCをお使いの方は一度は目にした「?」ではないでしょうか😅 -つまりGHCはデフォルトで`hSetEncoding stderr $ mkLocaleEncoding TransliterateCodingFailure`しているものと推測されます。 -いずれにせよ、エラーでプログラムが異常終了しないだけマシですね。 - -更に補足すると、GHCの文字コードについてより詳しい情報は、[GHC.IO.Encodingのドキュメント](https://hackage.haskell.org/package/base-4.10.1.0/docs/GHC-IO-Encoding.html)をご覧ください。 - -# Permission Deniedと言われたらビルドし直そう - -雑なまとめと言いつつ最初の一つ目が長くなってしまいましたが、ここからは簡単に言います。 -Windowsで`stack build`なり`ghc`なり`elm-make`なりとにかくいろいろ動かしていると、「Permission Denied」と言ったエラー(あるいはこれと似たようなメッセージのエラー)に出遭います。 -正直に言って私は原因はサッパリ分かってないのですが、このエラーは大抵の場合何度も同じコマンドを実行すれば再現しませんでした。 -一度や二度ではめげず、繰り返すのがポイントです 😅 -問題が起きているディレクトリーをウィルス対策ソフトのスキャン対象から外してみるとか、Dropboxの同期を一時的に止めてみる、といったこともやってみるといいかもしれません。 - -あ、あと、「Directory not empty」みたいなのもあったかな。これは同類のはずです。 - -# Cのライブラリーは... まぁ、頑張れ。 - -Pure Haskellなライブラリーであれば大体OKなんですが、残念ながらCのライブラリー(`lib***`みたいな名前でよくOSのパッケージマネージャーに登録されているやつですね)に依存したライブラリーは、Windowsでインストールするのは結構トラブることが多いです。 -まぁ、これはHaskellに限った話ではないでしょう。 - -対応方法は私が知る限り完全にケースバイケースなので、ここでは知っている対応例をいくつか挙げておきましょう。 - -- HDBC-sqlite3: - - [Windows版stackでもHDBC-sqlite3をビルドする - Qiita](https://qiita.com/igrep/items/d947ab871eb5b20b57e4) - - [MSYS2でHDBC-sqlite3をコンパイル - 北海道苫小牧市出身の初老PGが書くブログ](http://hiratara.hatenadiary.jp/entry/2017/01/29/110100) -- [Haskell - Haskellにてstackでiconvパッケージを利用する方法【Windows環境】(102462)|teratail](https://teratail.com/questions/102462) - -以上です! -それでは2018年もHaskell on Windows 10でHappy Hacking!! WSLなんて知らないぜ!🏁🏁🏁 diff --git a/preprocessed-site/posts/2018/about-ghc-exts-1.md b/preprocessed-site/posts/2018/about-ghc-exts-1.md deleted file mode 100644 index 091c6b49..00000000 --- a/preprocessed-site/posts/2018/about-ghc-exts-1.md +++ /dev/null @@ -1,855 +0,0 @@ ---- -title: GHC拡張ノック(Part 1) -headingBackgroundImage: ../../img/post-bg.jpg -headingDivClass: post-heading -subHeading: n番煎じのよく使うGHC拡張の紹介 -author: mizunashi_mana -postedBy: mizunashi_mana -tags: GHC, Language Extensions -date: May 15, 2018 -... ---- - -Haskell[^notice-haskell-standard-version]では各処理系で言語拡張を提供し,`LANGUAGE`プラグマというものを利用することで,言語拡張を利用することが許容されています.Haskellのデファクト標準的な処理系GHCも多くの言語拡張を提供しており,その拡張は**GHC拡張**と呼ばれています. - -[^notice-haskell-standard-version]: この記事では特に断らない限り,[Haskell2010][haskell-lang-report-url]を「Haskell標準」または「Haskell」と呼称します. - -今回は,このGHC拡張の簡単な紹介と,個人的に良く使う拡張についての簡単な紹介を,全3回に分けて行いたいと思います.対象としては,GHCでHaskellプログラミングをしたことがあり,通常のHaskellの構文や動作方法が分かっている人を考えています.また,この記事はあくまで簡単な紹介に留めるもので,付随する留意点や詳細な機能説明は,大事な箇所は漏らさないよう注意するつもりですが,全てを網羅するつもりはありませんのでその点は注意してください.もし,実際にGHC拡張を使用する際は,[GHCのユーザーガイド][ghc-user-guide-url]をよく読んでから使用するのが良いでしょう. - -# GHC拡張について - -## Haskellの言語拡張 - -Haskellには,言語拡張を取り込む方法が標準で提供されています.Haskell標準では,コンパイラプラグマというものが策定されており,これを通してコンパイラに追加情報を提供することができます.コンパイラプラグマは`{-#`と`#-}`で囲まれ,字句的にはコメントとして扱われます.標準では,インラインプラグマや特殊化プラグマの他に,`LANGUAGE`プラグマというものが策定されており,このプラグマを通して言語拡張を指定することができます. - -例えば,実装によって`CPP`と`ScopedTypeVariables`という名前の言語拡張が提供されており,それを使いたい場合,次のような文をモジュールの開始前に指定することで,言語拡張が有効になります. - -```haskell -{-# LANGUAGE CPP, ScopedTypeVariables #-} - -module A where -``` - -また,`LANGUAGE`プラグマを複数指定することもできます. - -```haskell -{-# LANGUAGE CPP #-} -{-# LANGUAGE ScopedTypeVariables #-} - -module A where -``` - -この機能を通して,多くのHaskell処理系では言語拡張を提供しています. - -## GHC拡張 - -Haskellのデファクト標準な処理系GHCも,多数の拡張を提供しており,この拡張がGHC拡張と呼ばれるものです.GHC拡張は,バージョン8.4.2現在,以下の数が提供されています[^notice-supported-extensions-option]. - -```bash -$ ghc --supported-extensions | wc -l -235 -``` - -[^notice-supported-extensions-option]: このオプションは,拡張を無効にするGHC拡張(例えば,`NoImplicitPrelude`拡張など)も含めて表示します.実際には`No`が付いている拡張を抜くと,提供されている数は120個になります. - -`--supported-extensions`オプションは,現在のGHCで使用できるGHC拡張を表示してくれるオプションです.ただ,GHC拡張は全てが独立した拡張ではなく,互いに依存しあった拡張が多く存在します.また,先頭に`No`がついている拡張は,そのGHC拡張を無効にするような拡張になっています [^notice-standard-disable-extensions] [^notice-both-disable-enable](例えば,`NoImplicitPrelude`拡張は`ImplicitPrelude`拡張を無効にする拡張です). - -[^notice-standard-disable-extensions]: Haskell標準では,ある拡張を無効にするといった機能は提供されていません.このため,GHCでは無効にする機能を1つの拡張として,Haskell標準に則った形で提供しています. -[^notice-both-disable-enable]: 有効にする拡張と無効にする拡張を両方指定した場合,GHCは指定された順番に沿って最後に指定された方を拡張として採用します. - -また,デフォルトで有効になっている拡張などもあります.例えば,`ImplicitPrelude`という拡張はデフォルトで有効になります.現在デフォルトのHaskell 2010をベースにしたモードでGHC 8.4.2を使用する場合,以下の拡張が[デフォルトで有効になります](https://github.com/ghc/ghc/blob/ghc-8.4.2-release/compiler/main/DynFlags.hs#L2022) [^notice-haskell2010-standard-exts] [^notice-default-extensions-by-ghc] [^notice-relaxed-polyrec]. - -* [`NondecreasingIndentation`](https://prime.haskell.org/wiki/NondecreasingIndentation): Haskellのレイアウトルールを変更する拡張です.この拡張を有効にすると,ネストされた`do`式の場合,インデントをしなくていいようになります. -* [`ImplicitPrelude`](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#rebindable-syntax-and-the-implicit-prelude-import): 暗黙的に`Prelude`モジュールがインポートされるようになる拡張です. -* [`MonomorphismRestriction`](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#switching-off-the-dreaded-monomorphism-restriction): [単相性制限](https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-930004.5.5)を課すようにする拡張です.この制限により,関数束縛でなく型注釈もない束縛変数の型は,デフォルティングルールによって単相化されます. -* [`TraditionalRecordSyntax`](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#traditional-record-syntax): レコード構文を有効にする拡張です.この拡張では,名前付きのフィールドを持つデータ型を定義し,それを使用することが可能になります. -* [`EmptyDataDecls`](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-EmptyDataDecls): コンストラクタを持たないデータ型の定義を許容する拡張です. -* [`ForeignFunctionInterface`](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/ffi-chap.html#extension-ForeignFunctionInterface): FFIが使えるようになる拡張です.この拡張により,`foreign import`構文を使用することで,HaskellからCの関数を読み込むことができるようになります. -* [`PatternGuards`](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#pattern-guards): `case`式において,通常のパターンに加えて,`<-`を使用してガードの中でさらにマッチした条件下でパターンマッチができるようになる拡張です.例えば,`case (x, y) of { (True, y) | False <- y -> True; _ -> False }`というような式が書けるようになります. -* [`DoAndIfThenElse`](https://prime.haskell.org/wiki/DoAndIfThenElse): `if`式の構文を,`then`と`else`の前に`;`を許容するよう変更する拡張です.これにより,`do`式において`then`や`else`をインデントする必要がなくなります. - -[^notice-haskell2010-standard-exts]: Haskell2010標準では,`Haskell2010`というプラグマをサポートすること,またHaskell98から新たにHaskell2010までに取り込まれた機能を切り離した`PatternGuards`/`NoNPlusKPatterns`/`RelaxedPolyRec`/`EmptyDataDecls`/`EmptyDataDecls`という拡張をそれぞれサポートすることが望ましいと規定されています.GHCも`Haskell2010`という拡張を指定できるようになっており,ここにあるほとんどはこの拡張を有効にした場合にも有効になります. -[^notice-default-extensions-by-ghc]: デフォルトで有効になる拡張のほとんどは,Haskell 2010を元にしたものです.ただし全てがそうというわけではありません.`NondecreasingIndentation`はHaskell標準にはない機能です.またGHCはHaskell 2010で規定されている仕様を全てデフォルトで取り込んでいる訳でもありません.特にHaskell標準ではデータ型の宣言に型制約を書くことができますが,GHCではデフォルトではできません.これを有効にする場合,[`DatatypeContexts`拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-DatatypeContexts)を有効にする必要があります. -[^notice-relaxed-polyrec]: GHCの内部では`RelaxedPolyRec`という拡張も一緒に有効になります.しかし,現在この拡張は実装上の問題でGHC上で無効にすることができないため,ドキュメント上からも削除されています.この記事でもGHCの方針に従って,この拡張は特に扱いませんのでご留意ください. - -歴史的経緯で生まれ,互換性のために残されているものの,現状使用が推奨されていない拡張もあります.他に実験的な拡張やかなり大胆な拡張も存在するため,GHC拡張を使用する際は[GHCのユーザーガイド][ghc-user-guide-url]をよく読んでから使用するのが良いでしょう. - -## GHC拡張の使い方 - -GHCでGHC拡張を使用する方法は,Haskell標準の`LANGUAGE`プラグマを使用する他に,幾つかあります.まず,GHCにオプションを渡して有効にする方法です.例えば,`NoImplicitPrelude`拡張と`Strict`拡張を有効にした状態で`Main.hs`をコンパイルしたい場合,次のように書けます. - -```bash -ghc -XNoImplicitPrelude -XStrict --make Main.hs -``` - -GHCでは`-X`の後に拡張名を続けることで,言語拡張を有効にしてコンパイルすることができます.通常は,`LANGUAGE`プラグマを使用するのが良いですが,何らかの事情で`LANGUAGE`プラグマを使用できない場合や,デフォルトで有効にしたい言語拡張がある場合などに便利でしょう.特にGHCiで言語拡張を有効にしたくなった場合,このオプションを`set`コマンドで指定すると良いでしょう. - -```haskell ->>> :set -XNoImplicitPrelude -XStrict -``` - -他にGHC拡張を有効にする方法として,`Cabal`の機能を活用する方法があります.`cabal`ファイルのビルド情報欄には,[`default-extensions`というフィールド](https://www.haskell.org/cabal/users-guide/developing-packages.html#pkg-field-default-extensions)を指定することができ,そこにデフォルトで有効にしたい言語拡張のリストを書くことで,その拡張を有効にした状態で`Cabal`がビルドを行ってくれます.例えば,`NoImplicitPrelude`拡張と`Strict`拡張をデフォルトで有効にしてビルドしたい場合,次のように書きます. - -```cabal -name: TestPackage -version: 0.0 -synopsis: Small package with a program -author: Angela Author -license: BSD3 -build-type: Simple -cabal-version: >= 1.2 - -executable program1 - build-depends: base - main-is: Main.hs - default-extensions: NoImplicitPrelude, Strict -``` - -# 主要なGHC拡張 - -以下では,個人的にデフォルトで有効化して使っている拡張を幾つか紹介します.なお,GHCのバージョンは8.4.2でHaskell2010モードで使用することを前提にしています. - -## Preludeの暗黙的な使用を抑制する - -この節では,以下の拡張を紹介します. - -* `NoImplicitPrelude`: [ユーザーガイド - NoImplicitPrelude拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-NoImplicitPrelude) - -Haskellでは,[Preludeモジュール][prelude-url]が暗黙的にimportされます.つまり,Haskellプログラムは暗黙に - -```haskell -import Prelude -``` - -と書いてあると,解釈されるということです.[Preludeモジュール][prelude-url]には,`Int`/`IO`といった基本的なデータ型や,`Eq`/`Functor`といった基本的な型クラス,`zip`/`putStrLn`といった基本的な関数が含まれています. - -[Preludeモジュール][prelude-url]の暗黙的なimportは,Haskellプログラムを簡潔に書く上では便利ですが,これを無効にしたい場合もあります. - -1. [Preludeモジュール][prelude-url]にあるデータ型や関数と同じ名前の,別モジュールの関数を使いたい時 -1. 別の代替となるpreludeパッケージを使う時 - -といった場合です.`NoImplicitPrelude`拡張はまさしくこのような場合に,[Preludeモジュール][prelude-url]を暗黙的にimportしないようにするGHC拡張です.1番目の理由の場合,この拡張をデフォルトで入れずモジュール度に指定すればいいと思いますが,私的には2番目の理由でこの拡張を使うためデフォルトで有効にしています.代替となるpreludeパッケージは幾つか存在しますが,主に - -* classy-prelude: [Hackageリンク](https://hackage.haskell.org/package/classy-prelude) -* protolude: [Hackageリンク](https://hackage.haskell.org/package/protolude) -* universum: [Hackageリンク](https://hackage.haskell.org/package/universum) -* basic-prelude: [Hackageリンク](https://hackage.haskell.org/package/basic-prelude) - -などがあります[^notice-rio].これらのパッケージを探すには[HackageのPreludeカテゴリ](https://hackage.haskell.org/packages/#cat:Prelude)を参照するといいでしょう. - -[^notice-rio]: 現在,Preludeの代替を目指す,[rio](https://hackage.haskell.org/package/rio)というパッケージが作成されています.このパッケージは現在まだprereleaseの段階で,[stack](https://github.com/commercialhaskell/stack)において実験的に使用されています.様々な最新のHaskellプログラミングの知見を取り入れており,標準のPreludeに大きく拡張を施しているため,Haskellで大規模な開発を行う場合注目する価値があるかもしれません. - -私の場合,classy-preludeを使っていますが,それも生で使用しているわけではなく,パッケージごとにpreludeモジュールを作って使用しています.Preludeは,最もよく使うものが提供されているモジュールですから,APIの変更の影響を最も強く受けます.それを外部パッケージに依存させると,パッケージ保守が結構大変です.もし,パッケージごとにpreludeモジュールを作っておけば,パッケージ側やGHCのバージョン変更の影響などでAPIが変更されても,そのモジュール内でフォールバックを設定することで他のモジュールに変更を持ち越す必要がなくなります.これを`NoImplicitPrelude`拡張と組み合わせ, - -```haskell -{-# LANGUAGE NoImplicitPrelude #-} - -module A where - -import MyPrelude - -... -``` - -と書くことで,保守がかなりしやすくなります. - -## 便利な構文の導入 - -### 新たなリテラル表記を可能にする - -この節では,以下の3つの拡張を紹介します. - -* `BinaryLiterals`: [ユーザーガイド - BinaryLiterals拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-BinaryLiterals) -* `NagativeLiterals`: [ユーザーガイド - NagativeLiterals拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-NegativeLiterals) -* `HexFloatLiterals`: [ユーザーガイド - HexFloatLiterals拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#ghc-flag--XHexFloatLiterals) - -Haskellには幾つかのリテラルが存在します.例えば,`'c'`は文字cを表すChar型のリテラルです.`100`は整数100を表す`Num a => a`型のリテラルで,`100.1`は浮動小数点数100.1を表す`Fractional a => a`型のリテラルになります.Haskell標準には他にも幾つかリテラルが存在しますが,特に数値は非常に多様な使われ方がなされるため,他の多くの言語はより強力なリテラル表現を持つことがあります.GHC拡張ではこの背景を元に,リテラルに対する幾つかの拡張を提供しています.`BinaryLiterals`は`Num a => a`型のリテラルに対して,`HexFloatLiterals`は`Fractional a => a`型のリテラルに対して,`NegativeLiterals`はどちらに対してもの拡張を,それぞれ提供します. - -数値型に対するリテラルは,既存のものでも数種類存在します.通常の数値表現`20`,オクテット(8進数)表現`0o24`,ヘックス(16進数)表現`0x14`の3つです.`BinaryLiterals`拡張は,これに加え`0b`を接頭辞に付けることでバイナリ(2進数)表現`0b10100`を可能にする拡張です. - -これらのオクテット表現やヘックス,バイナリ表現は浮動小数点数の表現はできません.しかし,浮動小数点数は実際にはIEEEの規格に則ったデータ表現になりますから,10進数表現よりも16進数表現の方が実態として分かりやすい場合があります.このため`HexFloatLiterals`拡張では,接頭に`0x`の付くヘックス表現でも浮動小数点数のリテラルを記述できるようにしています.この拡張によって,`0.25`は`0x0.4`と表記できるようになります.また,指数表記も10進方式のものではなく,ビット方式のものになります.指数表記には`e`ではなく`p`を使い,何ビット移動させるか(つまり,2の何乗を掛けるか)を書くようにします.例えば,`1.0`は`0x0.4p2`と表記できます.また,`0.125`は`0x0.4p-1`と表記できます. - -さて,Haskellには唯一の単項演算子`-`があります.この演算子を使用することで`negate 1`の代わりに`-1`という表記が可能になります.しかし,この演算子の結合度は非常に弱く,また二項演算子の`-`も存在することから`f -1`という表記は`(f) - (1)`というように解釈されてしまうなどの問題があり,非常に使い勝手が悪い演算子となっていました.また,Haskellの仕様上,`-128`という表現は最終的に`negate (fromInteger 128)`という式に[脱糖されます](https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-280003.4)が,例えば`Int8`などの,負数は`-128`まで扱えるが正数は`+127`までしか扱えないといったデータ型の場合に,この式は`fromInteger`で一度`+128`の値になってしまいオーバーフローを起こしてしまうという問題がありました.これを解決するため導入されたのが`NagativeLiterals`拡張です.この拡張を導入することで空白を挟まない`-1.0`などは1つのリテラルと解釈されるようになります.この拡張を導入後は,次のようになります. - -```haskell ->>> max -1 2 == max (-1) 2 -- before: max -1 2 == max - (1 2) -True ->>> data SamplePZ = SamplePZ deriving (Eq, Show) ->>> instance Num SamplePZ where { fromInteger i | i <= 0 = SamplePZ } ->>> -100 :: SamplePZ -- before: raise error -SamplePZ ->>> - 100 :: SamplePZ -*** Exception: ... ->>> instance Fractional SamplePZ where { fromRational r | r <= 0 = SamplePZ } ->>> -100.10 :: SamplePZ -- before: raise error -SamplePZ ->>> - 100.10 :: SamplePZ -*** Exception: ... -``` - -### 空のデータ型に対するより強力なサポートを導入する - -この節では,以下の2つの拡張を紹介します. - -* `EmptyCase`: [ユーザーガイド - EmptyCase拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-EmptyCase) -* `EmptyDataDeriving`: [ユーザーガイド - EmptyDataDeriving拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#ghc-flag--XEmptyDataDeriving) - -Haskellでは,コンストラクタを一切持たない型を定義できます.これは空のデータ型と呼ばれ,次のように書けます. - -```haskell -data Empty -``` - -このような型は`base`パッケージの`Data.Void`モジュールでも提供されており,有用な場合があります.しかし,Haskell標準ではこのようなデータ型に対するサポートが薄く,使用する上で不便な場面があります.このサポートを強化する拡張が,`EmptyCase`拡張と`EmptyDataDeriving`拡張です. - -`EmptyCase`拡張は,空のパターンマッチを書けるようにする拡張です.Haskell標準では,空のパターンマッチは書けません.つまり,`case x of {}`というような式が書けないということです.通常はデータ型は何らかのコンストラクタを持っていますから,このようなパターンマッチを書きたいと思う場面はないでしょう.しかし,空のデータ型においてこのようなパターンマッチを書きたいと思うことがあります. - -```haskell -f :: Empty -> a -f x = case x of {} -``` - -このような表記を可能にするのが`EmptyCase`拡張です.なお,このケース式は次のように書くのと同値になります. - -```haskell -f :: Empty -> a -f x = x `seq` error "Non-exhaustive patterns in case" -``` - -もう1つの`EmptyDataDeriving`拡張は,空のデータ型に対して`deriving`構文を使用できるようにする拡張です.空のデータ型は,通常のデータ型と違い`Eq`や`Show`などの型クラスインスタンスを[`deriving`することができません](https://www.haskell.org/onlinereport/haskell2010/haskellch11.html#x18-182014x6).つまり以下のようなことができません. - -```haskell -data Empty - deriving (Eq, Ord, Show) -``` - -しかし,これでは不便な場合があります.それを可能にするのが`EmptyDataDeriving`拡張です.この拡張では,`Eq`/`Ord`/`Show`/`Read`の4つが`deriving`可能になり,それぞれは次のようなインスタンスを生成します. - -```haskell -instance Eq Empty where - _ == _ = True - -instance Ord Empty where - compare _ _ = EQ - -instance Read Empty where - readPrec = pfail - -instance Show Empty where - showsPrec _ x = case x of {} -``` - -### 新たな基本構文を導入する - -この節では,以下の3つの拡張を紹介します. - -* `TupleSections`: [ユーザーガイド - TupleSections拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-TupleSections) -* `MultiWayIf`: [ユーザーガイド - MultiWayIf拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-MultiWayIf) -* `LambdaCase`: [ユーザーガイド - LambdaCase拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-LambdaCase) - -Haskellでは,タプルやラムダ抽象,セクション,`if`式や`case`式といった構文が導入されていますが,これらを組み合わせて多用する場合,幾つか冗長な表現が生まれる場合があります.その中でも頻出する表現に対して,新たな構文を提供するGHC拡張があります.それが,`TupleSections`,`MultiWayIf`,`LambdaCase`の3つの拡張です. - -Haskellには,セクションと呼ばれる二項演算子の部分適用を表す構文があります.また,Haskellではタプルにも独自の構文が充てがわれています.このタプルを使用する際,セクションのように部分適用を簡潔に書きたい場合があります.例えば,`\x -> (1, x)`という表現をもっと簡潔に書きたい場合があります.この場合は`(,) 1`というな表記が可能ですが,2番目に部分適用したい場合や,3つ組のタプルに部分適用したい場合などは非常に面倒です.このため,`TupleSections`拡張は`(1, )`という表記でタプルの部分適用を書ける構文を提供します.2つ以上空きがある場合は,左から引数を受け取っていくようになります.例えば,`(True, , "str", )`は`\x y -> (True, x, "str", y)`と同等です. - -`MultiWayIf`は名前の通り複数の条件をガード構文のように指定できる`if`式を提供する拡張です.つまり,以下のようなことがかけます. - -```haskell -f :: [Int] -> IO () -f xs = sequence_ $ do - x <- xs - pure $ if - | x <= 0 -> fail "non-positive number" - | x `mod` 15 == 0 -> putStrLn "FizzBuzz" - | x `mod` 3 == 0 -> putStrLn "Fizz" - | x `mod` 5 == 0 -> putStrLn "Buzz" - | otherwise -> print x -``` - -この`MultiWayIf`は次のように`case`式で書き換えることが可能です. - -```haskell -f :: [Int] -> IO () -f xs = sequence_ $ do - x <- xs - pure $ case () of - _ | x <= 0 -> fail "non-positive number" - _ | x `mod` 15 == 0 -> putStrLn "FizzBuzz" - _ | x `mod` 3 == 0 -> putStrLn "Fizz" - _ | x `mod` 5 == 0 -> putStrLn "Buzz" - _ | otherwise -> print x -``` - -3つ目の`LambdaCase`拡張は,ラムダ抽象と`case`式を組み合わせた際に良く使う表現をより簡潔に書けるようにする拡張です.この拡張を使うと,`\x -> case x of (a, b) -> a + b`というようなラムダ抽象を,`\case (a, b) -> a + b`と書けるようになります.もちろんレイアウトルールも`case-of`式と同じように作用するため,改行を含んだ式も書けます. - -```haskell -f :: Maybe Int -> Int -f = negate . \case - Nothing -> 0 - Just x -> x -``` - -### 正格化に対するサポートを導入する - -この節では,以下の3つの拡張を紹介します. - -* `BangPatterns`: [ユーザーガイド - BangPatterns拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-BangPatterns) -* `StrictData`: [ユーザーガイド - StrictData拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-StrictData) -* `Strict`: [ユーザーガイド - Strict拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-Strict) - -Haskellはデフォルトの評価戦略として,グラフ簡約の遅延評価を採用しています.これはリストや再帰に関する表現を非常に豊かにする反面,パフォーマンスを悪化させたりデバッグを困難にさせる場面が多いなどの負の面もあります.このためHaskell標準では,`seq`関数や正格フラグといった正格評価へのサポートも提供しています.しかし,このサポートは表現が冗長な場合が多く,使い勝手が悪い側面があります.この面を解決するための拡張が,`BangPatterns`,`StrictData`,`Strict`の3つの拡張です. - -再帰関数において,累積引数は多くの場合正格に計算した方が効率が良いですが,Haskell標準では以下のように書く必要がありました. - -```haskell -sum :: [Int] -> Int -> Int -sum xs y = y `seq` case xs of - x:xs' -> sum xs' (x + y) - [] -> y -``` - -このような`seq`による評価をより簡潔に書けるよう,`BangPatterns`拡張というものが提供されています.これはパターンを拡張し,バンパターンというものを導入します.このバンパターンは,通常のパターンに`!`を付けることで書けます.例えば,上の例はバンパターンを使うと以下のように書けます. - -```haskell -sum :: [Int] -> Int -> Int -sum xs !y = case xs of - x:xs' -> sum xs' (x + y) - [] -> y -``` - -バンパターンはパターンの1つですから,もちろん`let`式や`case`式でも`let !y = f x in y`や`case f x of !y -> y`というように使えます.また,`case x of (!y, z) -> y + z`というように部分パターンとしても有効です.バンパターンは[Haskellの`case`式の翻訳ルール][formal-semantics-of-pattern-matching]に次の規則を加えることで実現されます. - -```haskell -case v of { !pat -> e; _ -> e' } -≡ v `seq` case v of { pat -> e; _ -> e' } -``` - -Haskell標準では,データ型の宣言において,コンストラクタの引数に正格フラグというものを付けることが許容されています.このフラグをつけた引数は,正格に評価された後コンストラクタに渡されます.ただ,一般にデータ型の引数は正格な方が効率が良いため,データ型宣言時に正格フラグを付けるという慣習がありました.この慣習を打破するために導入されたのが,`StrictData`拡張です.`StrictData`拡張下のモジュールでは,データ型宣言時,コンストラクタの引数は全て正格フラグをつけているものとして扱われます.また,`~`というフラグが新たに導入され,このフラグをつけた引数の場合はHaskell標準化のデフォルトの動作,つまり引数は正格に評価されず遅延されるようになります.`StrictData`下で宣言された - -```haskell -data T = Normal Int | Strict !Int | Lazy ~Int -``` - -というデータ型は,通常のHaskellの以下のデータ型と同等になります. - -```haskell -data T = Normal !Int | Strict !Int | Lazy Int -``` - -`Strict`拡張は,`StrictData`拡張に加え,ほとんどのパターンを暗黙的にバンパターンにする拡張です.つまり,殆どの評価を正格にする拡張です.バンパターンに変わる箇所は,関数の引数,`let`/`where`句の束縛変数,`case`式のパターンマッチなどです.これらのパターンには,最外の場所に`!`が暗黙的に付与されます.例えば,`Strict`拡張下で定義された - -```haskell -f :: Int -> (Int, Int) -> Int -f x (z, y) = let zy = z * y in case x - z of z' -> z' ^ z -``` - -という関数は,`BangPatterns`拡張下のHaskellの以下の関数と同等になります. - -```haskell -f :: Int -> (Int, Int) -> Int -f !x !(z, y) = let !zy = z * y in case x - z of !z' -> z' ^ z -``` - -注意して欲しいのは,このバンパターンは`seq`に置き換わるため,WHNFまでしか評価されないということです.つまり,`!(z, y)`というパターンは単なる`(z, y)`と完全に同じです.またトップレベルの束縛にバンパターンを付与することは許されておらず,遅延されるということにも注意が必要です. - -### パターンマッチをより柔軟に扱えるようにする - -この節では,以下の2つの拡張を紹介します. - -* `ViewPatterns`: [ユーザーガイド - ViewPatterns拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-ViewPatterns) -* `PatternSynonyms`: [ユーザーガイド - PatternSynonyms拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-PatternSynonyms) - -GHC拡張では,Haskell標準のパターンをさらに強力なものにする拡張があります.`ViewPatterns`はビューパターンという新たなパターンを導入します.また,`PatternSynonyms`はパターンの別名を付けることができるようにする拡張です. - -Haskell標準にあるパターンガードは,非常に強力ですが,表現が非常に冗長になる場合があります.これを短縮して書けるように,`ViewPatterns`拡張はビューパターンというものを導入します.ビューパターンは,`->`の左側に式を,右側にパターンを書くことで,左の式に対象を適用して結果が右側のパターンにマッチした時,マッチするようなパターンです.例えば, - -```haskell -f ((`mod` 2) -> 0) = Nothing -f x = Just x -``` - -というように使用でき,`f 0`は`Nothing`を,`f 3`は`Just 3`をそれぞれ返すようになります.この関数宣言は,以下のパターンガードを用いて書いた関数と一致します. - -```haskell -f x | 0 <- x `mod` 2 = Nothing -f x = Just x -``` - -ビューパターンは[Haskellの`case`式の翻訳ルール][formal-semantics-of-pattern-matching]に次の規則を加えることで実現されます. - -```haskell -case v of { (e -> p) -> e1; _ -> e2 } -≡ case (e v) of { p -> e1; _ -> e2 } -``` - -`PatternSynonyms`拡張は,非常に強力で大きな拡張です[^notice-pattern-synonyms-bugs].`PatternSynonyms`拡張は名前の通り,パターンに別名を与えるパターンシノニム機能を提供します.パターンシノニムは通常の関数と同じように,次のように定義できます. - -[^notice-pattern-synonyms-bugs]: GHC 8.2.2の段階では,パターンシノニムはコンパイラがクラッシュするなどの非常に多くのバグを抱えていました.私は8.4.2をまだあまり試していませんが,パターンシノニムの仕様が非常に複雑なため,8.4.2でもまだバグを多く抱えている可能性があります.パターンシノニムをプロダクトで多用する場合,その点に注意した方が良いでしょう. - -```haskell -pattern Nil :: [a] -pattern Nil = [] - -pattern Cons :: a -> [a] -> [a] -pattern Cons x xs = x : xs - -{-# COMPLETE Nil, Cons #-} -``` - -このように定義したパターンは,以下のように使用できます. - -```haskell -len :: [a] -> Int -len (Cons _ xs) = 1 + len xs -len Nil = 0 -``` - -パターンシノニムは非常に便利な機能ですが,一方で注意する事項も幾つかあります. - -まず,パターンシノニムの定義は関数定義と非常に似ていますが,パターンの別名であることに注意してください.パターンシノニムの定義において変数が出現する場合,関数の引数のように錯覚してしまいがちですが,この変数にはパターンにマッチした時そのマッチした部分が当てがわれます.つまり,右の式でマッチしたものが左の変数に束縛されるため,左の変数に束縛された後右の式を実行する関数と,流れが逆になるということです.このため,パターンシノニムの引数の変数は必ず右に出現する必要があります.また,パターンシノニムの右側には変数を含むパターンしかかけません.そのため,式を書きたい場合,`ViewPatterns`拡張などを用いなければなりません.さらにパターンシノニムは,デフォルトではパターンの網羅性検査が非常に難しいため,網羅性検査を行わないようになっています.ただし,[`COMPLETE`プラグマ](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#complete-pragma)を用いてパターンシノニムの網羅条件を与えることで,その範囲で網羅性検査を行うようになります. - -パターンシノニムはパターンの種類に応じて3種類の書き方が存在します.上の単純なパターンシノニムは,双方向(bidirectional)パターンシノニムと呼ばれ,暗黙的にパターンの名前と等しい関数が作られます.この関数を用いることで,`[0, 1, 2]`の代わりに`Cons 0 (Cons 1 (Cons 2 Nil))`といった式も書くことができるようになります.ただし,このような関数が単純には作れないパターンも存在します.例えば,`(x, _)`というパターンに,`First x`というパターンシノニムを与えたい場合,この`First`に対する関数は`_`の部分に入れるべき値が分からないため,作りようがありません.このような関数が単純に作れないパターンシノニムは単方向(unidirectional)パターンシノニムと呼ばれ,双方向パターンシノニムが`=`を使って定義されるのに対し,次のように`<-`を使って書きます. - -```haskell -pattern First :: Int -> (Int, Bool) -pattern First x <- (x, _) -``` - -このパターンシノニムは`First`という関数は作らず,単純にパターンの別名だけを提供します.ただし,`First`関数の定義を次のように与えることが可能になっています. - -```haskell -pattern First :: Int -> (Int, Bool) -pattern First x <- (x, _) - where - First x | x < 0 = (x, False) - First x = (x, True) -``` - -また,パターンシノニムはパターンの評価順序にも注意する必要があります.例えば,次の例をみてください. - -```haskell -data Pair a b = Pair a b - -type Pair3 a b c = Pair a (Pair b c) - -pattern Pair3 :: a -> b -> c -> Pair3 a b c -pattern Pair3 x y z = Pair x (Pair y z) - -f :: Pair3 Bool Bool Bool -> Bool -f (Pair3 True True True) = True -f _ = False - -f' :: Pair3 Bool Bool Bool -> Bool -f' (Pair True (Pair True True)) = True -f' _ = False -``` - -この`f`と`f'`は評価順が異なり,`f (Pair False undefined)`が例外を投げるのに対し,`f' (Pair False undefined)`は`False`を返します.これは,パターンシノニムを使ったパターンマッチでは,自身のパターンを先に調べ,次に引数のパターンマッチを行うからです.つまり,`f`は以下と同等になります. - -```haskell -f :: Pair3 Bool Bool Bool -> Bool -f (Pair x (Pair y z)) | True <- x, True <- y, True <- z = True -f _ = False -``` - -パターンシノニムは,モジュールエクスポートを書く際にも注意が必要で,`module A (pattern Cons, pattern Nil) where ...`というように接頭に`pattern`をつける必要があります. - -### レコードに対するサポートを強化する - -この節では,以下の4つの拡張を紹介します. - -* `DuplicateRecordFields`: [ユーザーガイド - DuplicateRecordFields拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-DuplicateRecordFields) -* `OverloadedLabels`: [ユーザーガイド - OverloadedLabels拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-OverloadedLabels) -* `NamedFieldPuns`: [ユーザーガイド - NamedFieldPuns拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-NamedFieldPuns) -* `RecordWildCards`: [ユーザーガイド - RecordWildCards拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-RecordWildCards) - -Haskellのレコード構文は,便利な反面幾つか機能が劣る場面もあります.このため,GHCでは,レコードをより扱いやすくするための拡張を幾つか提供しています.それが,`DuplicateRecordFields`,`OverloadedLabels`,`NamedFieldPuns`,`RecordWildCards`の4つの拡張です[^notice-record-systems-bugs]. - -[^notice-record-systems-bugs]: GHCのレコードシステムの拡張は非常に強力ですが,その反面システムが非常に複雑になっています.このため,8.2.2の段階でコンパイラがクラッシュするなど非常に多くのバグを抱えていました.レコードシステムの仕様の改良は現在も進んでいますが,8.4.2でもまだバグを多く抱えている可能性があります.これらの拡張をプロダクトで多用する場合,その点に注意した方が良いでしょう.特に,GHC 8.0以降に導入された拡張には注意が必要です. - -Haskell標準では,同じモジュール内で同じフィールド名を持つ複数のレコード構文を使用したデータ型の定義を行うことができません.これはどのデータ型のフィールドかが曖昧であるようなプログラムを書けてしまうからですが,そういう状況に遭遇するとこの制約は非常に不便です.これを解決するのが,`DuplicateRecordFields`拡張です.`DuplicateRecordFields`拡張は,曖昧になるような式を書けなくする代わりに,同一モジュールの複数のデータ型が同じフィールド名を持つことを許容する拡張です.つまり,以下のようなことが可能になります. - -```haskell -data A = A { d :: Int } -data B = B { d :: Bool } -``` - -ただし,この拡張下では,曖昧なフィールドを用いたレコードのアップデート構文やフィールドの選択関数の使用の際は型を明記する必要があったり,モジュールのエクスポートリストで選択関数をエクスポートすることが出来なくなったりします. - -`OverloadedLabels`拡張は,`#foo`というような`#`から始まる新たな構文を導入します.`#foo`は`GHC.OverloadedLabels`モジュールの`fromLabel`メソッドにおいて`IsLabel "foo" a => a`というような型を持つ場合と同等になります.これを用いることで,同じフィールドを持つデータ型に対する選択関数を次のように書けます[^notice-ghcexts-for-overloaded-record-selector]. - -[^notice-ghcexts-for-overloaded-record-selector]: `OverloadedLabels`拡張はかなり最近入った拡張で,多数のGHC拡張,特に強力な型システムを前提にして書かれています.このため,選択関数の実装にもかなり多くのGHC拡張を使用しています.ここでは,特に解説しないのでそういうものだと思っておいてください.なお,このプログラムはプロダクションで使うことを前提にしていませんので,そこはご注意ください. - -```haskell -{-# LANGUAGE OverloadedLabels #-} -- the main extension -{-# LANGUAGE DataKinds #-} -- for Symbol kind -{-# LANGUAGE KindSignatures #-} -- for HasField's `l` parameter -{-# LANGUAGE MultiParamTypeClasses #-} -- for HasField and IsLabel classes -{-# LANGUAGE FunctionalDependencies #-} -- for HasField class -{-# LANGUAGE FlexibleInstances #-} -- for HasField instances -{-# LANGUAGE ScopedTypeVariables #-} -- for the IsLabel instance -{-# LANGUAGE DuplicateRecordFields #-} -- for A and B data types - -import GHC.OverloadedLabels (IsLabel(..)) -import GHC.TypeLits (Symbol) -import Data.Proxy (Proxy(..)) - -data A = A { d :: Int } -data B = B { d :: Bool } - -class HasField a (l :: Symbol) b | a l -> b where - selectField :: Proxy l -> a -> b - -instance HasField A "d" Int where - selectField _ (A x) = x - -instance HasField B "d" Bool where - selectField _ (B x) = x - -instance HasField a l b => IsLabel l (a -> b) where - fromLabel = selectField (Proxy :: Proxy l) -``` - -これを使うことで,`#d A { d = 0 }`は`0`を,`#d B { d = True }`は`True`を返してくるようになります.また,`#d`には型を明記しなくても型推論が働くようになります. - -さて他にレコードのパターンマッチやコンストラクトを非常に便利にしてくれる拡張として,`NamedFieldPuns`拡張と`RecordWildCards`拡張があります.レコードのパターンマッチは多くの場合冗長になりがちで,次のようなボイラープレートを書きがちです. - -```haskell -data A = A { x :: Int, y :: Bool } - -f :: A -> Int -f A{ x = x } = x + 1 -``` - -`NamedFieldPuns`拡張は,同等のことを次のように書けるようにする拡張です. - -```haskell -f :: A -> Int -f A{ x } = x + 1 -``` - -また,このパターンは旧来の書き方と合わせて書くこともできます. - -```haskell -g :: A -> Int -g A{ x, y = False } = - x -g A{ x } = x -``` - -さらにこの拡張は,コンストラクトの際も役に立ちます.`let x = 1 in A { x, y = True }`と書くとこの式は,`A { x = 1, y = True }`と書くのと同等になります. - -`NamedFieldPuns`拡張ではフィールド名を明記する必要がありましたが,`RecordWildCards`拡張はさらにフィールド名を明記する必要がなくなります.以下のように`{..}`と書くことで,全てのフィールドを展開してくれるようになります. - -```haskell -f :: A -> Int -f A{..} = x + 1 -``` - -また,部分的に明記することも可能で,その場合以下のように書きます. - -```haskell -g :: A -> Int -g A{ y = False, ..} = -x -g A{..} = x -``` - -コンストラクトの際も,この拡張は有効です.`let x = 1 in A { y = True, ..}`と書いた場合,`A { x = 1, y = True }`と書くのと同等になります. - -### 型演算子を導入する - -この節では,以下の拡張を紹介します. - -* `TypeOperators`: [ユーザーガイド - TypeOperators拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-TypeOperators) - -Haskellではユーザー定義の関数やデータ型のコンストラクタにおいて,演算子表記のものも定義できるようになっています.例えば,以下のようにです. - -```haskell -data Pair a b = a :*: b -infixl 7 :*: - -(&) :: a -> (a -> b) -> b -x & f = f x -infixl 1 & -``` - -しかしHaskell標準では,型を定義する場合そのようなことはできません.これを可能にするのが,`TypeOperators`拡張です.この拡張の有効下では, - -```haskell -type a + b = Either a b -infixr 5 + -``` - -ということが可能になります.ただし,このように定義した型演算子は,同じ名前の値としての演算子があった場合区別ができません.このため,モジュールのエクスポートリストを書く際,型演算子か値レベルの演算子かの区別が付かなくなった場合,値レベルの方が優先されます.この時,型演算子を明示したい場合,`type`を付けます[^notice-explicit-namespaces]. - -[^notice-explicit-namespaces]: この機能は型演算子を定義しないで再エクスポートなどをする場合にも使用されるため,[`ExplicitNamespaces`拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-ExplicitNamespaces)として切り離されています. - -```haskell -{-# LANGUAGE TypeOperators #-} - -module A - ( type (+) - ) where - -type a + b = Either a b -``` - -### 型クラスを拡張する - -この節では,以下の4つの拡張を紹介します. - -* `MultiParamTypeClasses`: [ユーザーガイド - MultiParamTypeClasses拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-MultiParamTypeClasses) -* `FlexibleContexts`: [ユーザーガイド - FlexibleContexts拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-FlexibleContexts) -* `FlexibleInstances`: [ユーザーガイド - FlexibleInstances拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-FlexibleInstances) -* `InstanceSigs`: [ユーザーガイド - InstanceSigs拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-InstanceSigs) - -Haskellの型クラスは非常に強力な機構です.しかしながら,Haskell標準の型クラスの構文は非常に制約がきつく,これらを緩和したいと思うことがよくあります.このため,GHCでは制約を緩和する拡張をいくつか提供しています.それが,`MultiParamTypeClasses`,`FlexibleContexts`,`FlexibleInstances`,`InstanceSigs`の4つの拡張です. - -Haskell標準では,クラスは1つの変数しか持てません.なので,次のような型クラスは作れません. - -```haskell -class C a b -``` - -これは非常に不便な制約なため,複数のパラメータを使うような型クラスを許容する拡張が`MultiParamTypeClasses`拡張です.この拡張により,上のコードが許容されるようになる他,以下のように変数が全くない型クラスも宣言することができるようになります. - -```haskell -class Nullary -``` - -また,Haskell標準では,メソッドにおいてクラスの型変数に型制約をかけるということも許容されていませんが,`MultiParamTypeClasses`拡張ではこれも可能にします[^notice-constrained-class-methods].これによって以下のようなクラス定義も書けるようになります. - -[^notice-constrained-class-methods]: この機能は[`ConstrainedClassMethods`拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-ConstrainedClassMethods)として切り離されており,`MultiParamTypeClasses`拡張を有効にすると一緒に有効になります. - -```haskell -class Setable s a where - elem :: Eq a => a -> s a -> Bool -``` - -Haskell標準では,型制約の解決を安全に,しかも単純にするために,[型注釈における制約の書き方](https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-660004.1.3)や[クラス定義,インスタンス定義の際の制約の書き方](https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-760004.3.1)を大きく制限しています.しかし,より複雑な型制約を書きたい時が往々にしてあります.そこで,この制限を緩め,クラス階層が非循環である場合には許容するようにする拡張が,`FlexibleContexts`拡張です.この拡張下では, - -```haskell --- valid -class (Monad m, Monad (t m)) => Transform t m where - lift :: m a -> (t m) a - --- valid -f :: Functor Maybe => () -f = () - --- invalid -class A a => B a -class B a => A a -``` - -となります. - -`FlexibleInstances`拡張も`FlexibleContexts`拡張と同じく,Haskell標準での[型クラスインスタンスの書き方](https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-770004.3.2)の制限を,[停止制限](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#instance-termination)を守る場合に許容するというように緩和する拡張です.停止制限は簡単に言ってしまえば,インスタンス宣言において,型制約がインスタンスより小さく[^notice-smaller-constraint],型関数を使っていないというものです[^notice-instance-termination-rule].この拡張下では, - -[^notice-smaller-constraint]: 型制約が小さいとは,型変数とコンストラクタと変数の組の出現が少ないということです. -[^notice-instance-termination-rule]: より正確には,`FunctionalDependencies`に対する制限もありますが,ここでは割愛します. - -```haskell --- valid -instance C1 (Maybe [a]) - --- valid -instance C2 a a => C2 [a] [a] - --- valid -instance (Eq a, Show b) => C3 a b - --- valid -instance (Show a, Show (s a)) => Show (S s a) - --- invalid -instance C4 a => C4 a - --- invalid -instance C2 a a => C1 [a] - --- invalid -instance Functor [] => C1 a -``` - -となります.また,この拡張下では,型シノニムをインスタンスにすることもできます[^notice-type-synonym-instances]. - -[^notice-type-synonym-instances]: この拡張は,[`TypeSynonymInstances`拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-TypeSynonymInstances)として切り離されており,`FlexibleInstances`拡張を有効にすると一緒に有効になります. - -```haskell -type List a = [a] - --- Instead of `instance C [a]` -instance C (List a) -``` - -ただし,型シノニムを使う場合そのシノニムの引数は全て適用しなければならないことに注意が必要です. - -Haskell標準では,型クラスインスタンスの定義時,そのメソッドの型注釈は書けないようになっています.しかし,複雑な型クラスインスタンスを書く際,メソッドの型注釈を書きたい場合があります[^notice-instance-sigs-for-scoped-type-variables].これを可能にするのが`InstanceSigs`拡張です.`InstanceSigs`拡張の元では,以下のようなインスタンス宣言が書けます. - -[^notice-instance-sigs-for-scoped-type-variables]: 特に`ScopedTypeVariables`拡張を指定する場合,型注釈は必要です. - -```haskell -data A = A - -instance Eq A where - (==) :: A -> A -> Bool - A == A = True -``` - -### 型ワイルドカードをより柔軟に扱う - -この節では,以下の拡張を紹介します. - -* `NamedWildCards`: [ユーザーガイド - NamedWildCards拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-NamedWildCards) - -GHCには型ワイルドカードという機能があります.この機能は,`_`と型シグネチャ上で書いておくと,そこの部分の型を推論してエラーメッセージとして表示してくれる機能です.この機能は,以下のように部分的に記述したり複数指定したりすることも可能です. - -```haskell --- Inferred type: (a, b) -> (a, Maybe a1) -ignoreSecond :: _ -> _ -ignoreSecond (x, _) = (x, Nothing) -``` - -これを活用すれば,複雑な型をある程度ヒントを与えた状態で推論してもらい,型を追記するプログラミングスタイルや,GHCが実際に型をどう推論するかを見るための補助に応用できます.しかし,例えば`ignoreSecond`が引数と返り値で型が同じであるという情報が分かっていた場合に,これをヒントとして伝えたい場合がありますが,型ワイルドカードでそれを伝える方法はありません.これを解決するのが`NamedWildCards`拡張です.この拡張を使うと,以下のようなプログラムに対しても,接頭に`_`が付いている型をワイルドカードとみなして,エラーメッセージで型の推論結果を表示してくれるようになります. - -```haskell --- Inferred type: (a, Maybe a1) -> (a, Maybe a1) -ignoreSecond :: _a -> _a -ignoreSecond (x, _) = (x, Nothing) -``` - -### 新たな表記法の導入 - -この節では,以下の2つの拡張を紹介します. - -* `Arrows`: [ユーザーガイド - Arrows拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-Arrows) -* `RecursiveDo`: [ユーザーガイド - RecursiveDo拡張](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#extension-RecursiveDo) - -Haskellでは,モナドを扱いやすくするための,do構文という専用の構文が用意されています.この構文はHaskellプログラミングにおいて広く利用されています.GHCでは,これに加え`Arrow`と`MonadFix`というクラスに対しての専用の構文も提供しています.これはGHC拡張で実装されており,それぞれ`Arrows`拡張,`RecursiveDo`拡張を有効にすることで使用可能です. - -`Arrow`クラスは,モナドの一般化として導入されました[^ref-generalising-monads-to-arrow].このクラスには,モナドの`do`構文と同様に,クラスメソッドだけの式に脱糖できる構文が考案され,GHC拡張として実装されています.それが`Arrows`拡張で利用できる`proc`構文です. - -[^ref-generalising-monads-to-arrow]: ["Generalising Monads to Arrows"](https://dl.acm.org/citation.cfm?id=347246), John Hughes, in Science of Computer Programming 37, pp. 67–111, May 2000 - -例えば,`Arrow`クラスのメソッドを使った次のような関数は, - -```haskell -doSomething :: Arrow a => a Int Int -> a Int Int -> a Int Int -> a Int Int -doSomething f g h - = arr (\x -> (x + 1, x)) - >>> first (f >>> (arr (\y -> 2 * y) >>> g) &&& returnA >>> arr snd) - >>> arr (\(y, x) -> (x, x + y)) - >>> arr (\(x, z) -> (z, x * z)) - >>> second h - >>> arr (\(z, t) -> t + z) -``` - -`proc`構文を使うと, - -```haskell -doSomething :: Arrow a => a Int Int -> a Int Int -> a Int Int -> a Int Int -doSomething f g h = proc x -> do - y <- f -< x + 1 - g -< 2 * y - let z = x + y - t <- h -< x * z - returnA -< t + z -``` - -というように書けます[^notice-arrow-syntax-reduced].また,`ArrowLoop`クラスの`loop`メソッドに変換される,`rec`構文も搭載されており次のようなフィードバック制御を相互再帰で行うプログラムを書くことができます. - -[^notice-arrow-syntax-reduced]: 一見,この構文は単純な脱糖を行うと脱糖後のプログラムが非常に冗長になるように思えます.しかし,`Arrow`クラスのメソッドに設けられている書き換え規則によって,最終的に妥当な大きさまで脱糖後のプログラムが小さくなってくれます. - -```haskell -counter :: ArrowLoop a => (Int -> a Int Int) -> a Bool Int -counter delay = proc reset -> do - rec output <- returnA -< if reset then 0 else next - next <- delay 0 -< output + 1 - returnA -< output -``` - -`proc`構文については[Arrow syntax](https://www.haskell.org/arrows/syntax.html)のページにまとめられている他,[提案論文](http://www.staff.city.ac.uk/~ross/papers/notation.html)にて変換規則を確認することが可能です. - -さて,もう1つの`MonadFix`クラスは,モナドを拡張し,再帰的なバインディングを許すようなものです.このクラスを元に,`RecursiveDo`拡張はdo構文をさらに拡張します.具体的には,次のように使用できる`rec`という構文を新たに導入します. - -```haskell -doSomething :: [Int] -doSomething = do - rec x <- [y, y * 10] - y <- [1, 2] - pure $ x + y -``` - -この関数は,次のように`MonadFix`クラスのメソッド`mfix`を使った関数と同等です. - -```haskell -doSomething :: [Int] -doSomething = do - (x, y) <- mfix $ \~(x, y) -> do - x <- [y, y * 10] - y <- [1, 2] - pure (x, y) - pure $ x + y -``` - -また,`rec`を省略して書ける`mdo`という構文も提供されます. - -```haskell -doSomething :: [Int] -doSomething = mdo - x <- [y, y * 10] - y <- [1, 2] - pure $ x + y -``` - -`mdo`構文は,それぞれの文と変数の依存関係を解析し,自動的に`rec`ブロックに分けてくれます.後は,その分けられた`rec`文を`mfix`に翻訳することで,通常の`do`構文に翻訳することができます.例えば, - -```haskell -mdo - a <- m - b <- f a c - c <- f b a - z <- h a b - d <- g d e - e <- g a z - pure c -``` - -という式は, - -```haskell -do - a <- m - (b, c) <- mfix $ \~(b, c) -> do - b <- f a c - c <- f b a - pure (b, c) - z <- h a b - (d, e) <- mfix $ \~(d, e) -> do - d <- g d e - e <- g a z - pure (d, e) - pure c -``` - -という式に翻訳されます.`mdo`と`rec`の変換規則は,[提案論文](https://dl.acm.org/citation.cfm?doid=581690.581693)にて確認が可能です. - -# 次回予告 - -今回は,GHC拡張の簡単な紹介と使い方について,それから個人的にデフォルトで有効化している,Preludeの暗黙的なインポートを抑制する拡張,新たな構文を導入する拡張を紹介しました. - -次回は,他のデフォルトで有効化している拡張について紹介したいと思います. - -# 参考文献 - -* [GHC 8.4.2 User's Guide](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/) - - [GHC 8.4.2 User's Guide - 9. GHC Language Features](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/glasgow_exts.html#options-language) - - [GHC 8.4.2 User's Guide - 10. Foreign function interface (FFI)](https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/ffi-chap.html) -* [What I Wish I Knew When Learning Haskell - Language Extensions](http://dev.stephendiehl.com/hask/#language-extensions) -* [Guide to GHC Extensions - Language Standards](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/guide-to-ghc-extensions/language-standards) -* [Haskell 2010 Language Report][haskell-lang-report-url] -* [Cabal reference](https://www.haskell.org/cabal/users-guide/cabal-projectindex.html) - -[prelude-url]: https://www.stackage.org/haddock/lts-10.8/base-4.10.1.0/Prelude.html -[ghc-user-guide-url]: https://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/ -[haskell-lang-report-url]: https://www.haskell.org/onlinereport/haskell2010/ -[formal-semantics-of-pattern-matching]: https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-610003.17.3 diff --git a/preprocessed-site/posts/2018/derive-json-no-prefix.md b/preprocessed-site/posts/2018/derive-json-no-prefix.md deleted file mode 100644 index f2b4ffca..00000000 --- a/preprocessed-site/posts/2018/derive-json-no-prefix.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: deriveJsonNoPrefixをリリースしました -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -subHeading: レコードラベルにprefixを着けざるを得ない人達に送るライブラリーです -author: Yuji Yamamoto -postedBy: Yuji Yamamoto(@igrep) -date: July 18, 2018 -tags: -... ---- - -前回の更新からちょっと時間が空いてしまいました 💦 -小ネタです。掲題の通り[deriveJsonNoPrefix](http://hackage.haskell.org/package/deriveJsonNoPrefix)というパッケージをリリースしました。 -地味に有用だと思うので、[README](https://gitlab.com/igrep/deriveJsonNoPrefix/blob/master/README.md)をやや意訳気味に翻訳して記事にします。 -十分に単純なので、仕様が変わることもまさかないでしょうし。 - -以下、[こちらのコミットの時点のREADME](https://gitlab.com/igrep/deriveJsonNoPrefix/blob/6114e0fc55cf5b57a771871e53971a51592f618b/README.md)の翻訳です。 - -# deriveJsonNoPrefix - -プレフィックスに優しい`ToJSON`と`FromJSON`のインスタンスを定義するTemplate Haskellのマクロを提供します。 - -## 例 - -こんな感じのJSONを作りたいとしましょう: - -```json -{ - "id": "ID STRING", - "max": 0.789, - "min": 0.123 -} -``` - -きっと[ToJSON](http://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#t:ToJSON)(おそらくそれに加えて[FromJSON](http://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#t:FromJSON)も)のインスタンスを自動的に定義するための、次のようなレコード型を定義したくなるでしょう。 - -```hs -{-# LANGUAGE TemplateHaskell #-} - -import Data.Aeson.TH - -data SomeRecord = SomeRecord - { id :: String - , max :: Double - , min :: Double - } deriving (Eq, Show) - -$(deriveToJSON defaultOptions ''SomeRecord) -``` - -しかし、こんなレコード型は定義すべきではありません。 -`id`も`max`も`min`も、`Prelude`に定義済みなのですから! - -この問題を回避するために、レコードラベルに型の名前をプレフィックスとして加える、ということをわれわれはよくやります。 - -```hs -data SomeRecord = SomeRecord - { someRecordId :: String - , someRecordMax :: Double - , someRecordMin :: Double - } deriving (Eq, Show) -``` - -そして、`deriveToJSON`にデフォルトと異なるオプションを渡して実行します。 - -```hs -deriveToJSON Json.defaultOptions { fieldLabelModifier = firstLower . drop (length "SomeRecord") } ''SomeRecord - -firstLower :: String -> String -firstLower (x:xs) = toLower x : xs -firstLower _ = error "firstLower: Assertion failed: empty string" -``` - -`fieldLabelModifier`オプションは文字通り、対象のレコードをJSONに変換するとき、あるいはJSONから対象のレコードの値に変換する時、レコードのラベルを変換する関数を設定するために使います。 -👆の場合、プレフィックスである`SomeRecord`の文字数分レコードラベルから`drop`して、先頭の文字(`someRecordId`で言えば`Id`の`I`に相当します)を小文字に変換しているのがわかるでしょうか? - -そう、これが`deriveToJsonNoTypeNamePrefix`がやっていることとほぼ同等のことです。 -`deriveToJsonNoTypeNamePrefix`は、実質次のように定義されています。 - -```hs -deriveToJsonNoTypeNamePrefix :: Name -> Q [Dec] -deriveToJsonNoTypeNamePrefix deriver name = - deriveToJSON Json.defaultOptions { fieldLabelModifier = dropPrefix name } name - -dropPrefix :: Name -> String -> String -dropPrefix name = firstLower . drop (length $ nameBase name) - -firstLower :: String -> String -firstLower (x:xs) = toLower x : xs -firstLower _ = error "firstLower: Assertion failed: empty string" -``` - -結果、これからは`fieldLabelModifier`をもう自分で定義する必要がありません!🙌 - -```hs -import Data.Aeson.DeriveNoPrefix - -$(deriveJsonNoTypeNamePrefix ''SomeRecord) -``` - -👆 の`deriveJsonNoTypeNamePrefix` は [deriveJSON](https://hackage.haskell.org/package/aeson/docs/Data-Aeson-TH.html#v:deriveJSON)と同様に、`ToJSON`と`FromJSON`のインスタンス、両方を生成します。 -もちろん、`FromJSON`のインスタンスを生成するときのオプションとしても、プレフィックスを削除するための`fieldLabelModifier`を渡してくれます! - -## 同じ問題を解決するほかのライブラリー - -- [extensible](https://hackage.haskell.org/package/extensible). -- など、`ToJSON`・`FromJSON`のインスタンスが定義されたextensible recordを提供するライブラリー - -なので、そうしたextensible recordを提供するライブラリーが学習コストや依存関係などの事情で「重たい」と感じたときにこのパッケージを使ってください。 diff --git a/preprocessed-site/posts/2018/ghc-proposal-and-patch.md b/preprocessed-site/posts/2018/ghc-proposal-and-patch.md deleted file mode 100644 index 04495c37..00000000 --- a/preprocessed-site/posts/2018/ghc-proposal-and-patch.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: GHCへの変更提案とパッチ送付の手順例 -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -subHeading: ghc-proposals, Trac ticket, Phabricator, ... -author: takenbu.hs -postedBy: takenbu.hs -date: February 11, 2018 -... ---- - -## はじめに - -Haskellのコンパイラの1つであるGHCは、オープンソースソフトウェア(OSS)のプロジェクトとして今も活発に開発が進められています。 -個人の経験や経歴や肩書きや権限などに関わらず、誰でもGHCの開発にすぐに参加することができます。 - -ここでは、GHCに新しい変更を提案し実装するための、以下の手順例を紹介します。 - -1. [変更提案](#ch1) (GitHubのghc-proposals リポジトリ上にて実施) -2. [パッチ送付](#ch2) (PhabricatorのHaskell.org インスタンス上にて実施) - -GHCに改善したい点があれば、誰でも変更提案が可能です。 -提案のハードルは案外高いものではありません。GHC開発では、新たなcontributionが歓迎されています。 -仮に提案やパッチがreject判断されるとしても、GHCの開発者と直接やり取りする良い機会が得られます。 - -以下では、数値リテラルの構文を変更する単純な例をもとに、変更提案やパッチ送付の手順例を紹介します。(文章だらけになってしまいましたがご容赦を 😊 ) - - - ------- - -## 1. 変更提案(proposal) {#ch1} - -### 概要 - -GHCは、コンパイラ本体やライブラリやツールチェーンなど多くの要素で構成されていますが、ここではコンパイラ本体への変更提案の手順について紹介します。 - -GHCのコンパイラ本体の開発では、[ユーザーに見える(user-visible)振る舞い](https://github.com/ghc-proposals/ghc-proposals#what-is-a-proposal)等を変更(追加・修正・削除など)するための提案(proposal)手順が定められています。 -事前の調整や権限などを必要とせず、GitHubへのpull requestを通じて誰もが提案できます。 - -なお、変更提案(仕様)のプロセスと、修正パッチ送付(実装)のプロセスは、分離されています。必ずしも、変更提案者が実装まで行う必要はありません。 - - -### 変更提案の正確な手続き - -提案の具体的な手続きについては、以下に記載されています。よく読んでおきましょう。 - -* [https://github.com/ghc-proposals/ghc-proposals#ghc-proposals](https://github.com/ghc-proposals/ghc-proposals#ghc-proposals) - -変更提案は、提案書を書いて以下の場所(リポジトリ)に、pull requestを送ることで行えます。 - -* [https://github.com/ghc-proposals/ghc-proposals/pulls](https://github.com/ghc-proposals/ghc-proposals/pulls) - - -### 変更提案のおおまかな流れ - -[提案の流れ](https://github.com/ghc-proposals/ghc-proposals#what-is-the-timeline-of-a-proposal)は、ざくっと以下の通りです。 - -* 提案の作成 - * GitHub上で、[ghc-proposals](https://github.com/ghc-proposals/ghc-proposals)のリポジトリをforkする [(例)](https://github.com/takenobu-hs/ghc-proposals) - * forkしてきた自分のリポジトリで作業用のブランチを作る [(例)](https://github.com/takenobu-hs/ghc-proposals/tree/numeric-underscores) - * proposalsディレクトリの下に、"0000-プロポーザル名.rst"のファイル名で[提案用のファイルを作る](https://github.com/ghc-proposals/ghc-proposals#how-to-start-a-new-proposal) [(例)](https://github.com/takenobu-hs/ghc-proposals/blob/numeric-underscores/proposals/0000-numeric-underscores.rst) - * "Motivation"などの[必要な項目](https://github.com/ghc-proposals/ghc-proposals#what-should-a-proposal-look-like)を、[reStructuredText](http://docs.sphinx-users.jp/rest.html)の書式に従い記述する [(例)](https://github.com/takenobu-hs/ghc-proposals/blob/numeric-underscores/proposals/0000-numeric-underscores.rst) -* 提案の送付 - * GitHub上で、ghc-proposalsのリポジトリに、pull requestを送る [(例)](https://github.com/ghc-proposals/ghc-proposals/pull/76) - * 確定したpull requestのURLを、提案用のファイルの"This proposal is discussed at this pull request."の箇所に記載してから、再度commitし直す [(例)](https://github.com/takenobu-hs/ghc-proposals/commit/61149ee277aadc6bd46e0ad35aeb529f02da1182#diff-1128b179eb6630a402469b59a8a7dce6) - * pull requestの Conversationのところに、"Rendered"という文字で提案ファイルへのリンクを貼っておく [(例)](https://github.com/ghc-proposals/ghc-proposals/pull/76#issue-261822915) -* 提案についての議論 - * pull request上で、[議論する](https://github.com/ghc-proposals/ghc-proposals#discussion-goals) [(例)](https://github.com/ghc-proposals/ghc-proposals/pull/76) - * フィードバックがあれば、提案ファイルを修正する - * 議論期間を充分に(一ヶ月くらいは)設ける -* 提案の判断 - * 議論が収束したら、[GHC Steering Committee](https://github.com/ghc-proposals/ghc-proposals#who-is-the-committee) へ、[判断依頼](https://github.com/ghc-proposals/ghc-proposals#how-to-bring-a-proposal-before-the-committee)をかける [(例)](https://github.com/ghc-proposals/ghc-proposals/pull/76#issuecomment-339952996) - * GHC Steering CommitteeがAccepted/Rejectedを判断する [(例)](https://github.com/ghc-proposals/ghc-proposals/pull/76#event-1341434473) - * Acceptedなら、Tracで[ticketを登録](https://ghc.haskell.org/trac/ghc/newticket?type=task)する [(例)](https://ghc.haskell.org/trac/ghc/ticket/14473) - * 次は、コード修正パッチの作成・送付フェーズへ - - -### 変更提案の例 - -数値リテラルの構文を変更する場合の、具体的な変更提案の例を紹介します。 - -* [変更提案の初版](https://github.com/takenobu-hs/ghc-proposals/blob/0a694636560ca37d9b76e56bddc43c6bf1c9348a/proposals/0000-numeric-underscores.rst) | [最終的な変更提案](https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0009-numeric-underscores.rst) -* [議論フェーズの例](https://github.com/ghc-proposals/ghc-proposals/pull/76) - -その他の提案の例は以下にたくさんあります。 - -* [Open中の提案](https://github.com/ghc-proposals/ghc-proposals/pulls?q=is%3Aopen+is%3Apr) -* [Close済みの提案](https://github.com/ghc-proposals/ghc-proposals/pulls?q=is%3Apr+is%3Aclosed) - - -### いくつかのポイントなど - -* 他の良い提案が参考になります (同じ種類の提案や議論がうまく進んでいる提案などから、色々な観点を学べます。) -* 数カ月単位で気長に根気よくやる(開発者は全員がボランティアで忙しい。) -* 提案してよいか迷う場合は、事前に[ghc-devsのML(メーリングリスト)](https://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs)などで相談してもよい -* 英語の精度を必要以上に気にする必要はない。日本語でしっかり考える。あとは短い文に区切って、Google翻訳にでも。 - -提案プロセスはGitHub上で行うものです。操作ミスがあったところでやり直しは何度でも行えます。失敗やミスを不必要に怖れる必要はありません。 -また、多くの提案はAcceptedに至らないこともあるので、結果を恥ずかしがる必要もありません。提案の結果に関わらず、提案とその議論自体が、他の開発者に新たな観点や気づき・刺激を提供できます。 - -それでは、提案プロセスをお楽しみ! - - - ------- - -## 2. パッチ送付(patch) {#ch2} - -### 概要 - -GHCへの変更提案に対するコード修正は、パッチを作成して送付することにより行われます。 -ここでは、コード開発ツールであるPhabricatorのdifferential機能を用いる、標準的なパッチ送付の手順について紹介します。 - -なお、修正パッチはGitHubのpull requestを通じても送付できますが、後のコードレビューのフェーズを考慮すると、Phabricatorを用いるこの手順が効率的です。 - - -### パッチ送付の正確な手続き - -パッチ作成から送付についての具体的な手続きについては以下に記載されています。 - -* [How to contribute a patch to GHC](https://ghc.haskell.org/trac/ghc/wiki/WorkingConventions/FixingBugs) -* [Using Phabricator for GHC development](https://ghc.haskell.org/trac/ghc/wiki/Phabricator) - -また、Phabricatorの詳細な操作手順については、以下に解説記事があります。 - -* [Contributing to GHC via Phabricator](https://medium.com/@zw3rk/contributing-to-ghc-290653b63147) - - -### パッチ送付のおおまかな流れ - -パッチ送付の流れは、ざくっと以下の通りです。 - -* パッチの作成 - * GHCのbuild/validate用環境を[整えておく](https://ghc.haskell.org/trac/ghc/wiki/Building/Preparation) - * GHCのリポジトリを[cloneする](https://ghc.haskell.org/trac/ghc/wiki/Building/GettingTheSources#CloningHEAD) - * 事前にmasterブランチ上で、[buildできることを確認しておく](https://ghc.haskell.org/trac/ghc/wiki/Building/QuickStart) (master自体がfailしていることがあるため。) - * 事前にmasterブランチ上で、[validateが正常終了することを確認しておく](https://ghc.haskell.org/trac/ghc/wiki/TestingPatches#Locally) (master自体がfailしていることがあるため。) - * 作業用のブランチを作り、コードを修正する - * 修正が1つのcommitにまとまっていると、後のarcコマンドでのパッチ送付がラクです。"git merge --squash"でまとめられます。 - * [テストケースを追加する](https://ghc.haskell.org/trac/ghc/wiki/Building/RunningTests/Adding) [(例)](https://phabricator.haskell.org/D4235#change-AHneoV84zpis) - * 必要に応じて[ユーザーガイド](https://ghc.haskell.org/trac/ghc/wiki/Commentary/UserManual)に変更機能についての説明を追加する [(例)](https://phabricator.haskell.org/D4235#change-0p_6dVtsoCP3) - * 修正コードにてbuildできることを確認しておく(必ず行う) - * 修正コードにてvalidateが正常終了することを確認しておく(必ず行う) -* パッチの送付 - * Phabricator用のコマンドラインツール[Arcanistをインストールする](https://ghc.haskell.org/trac/ghc/wiki/Phabricator#TheCLI:Arcanist) ([arcanistツールの説明](https://secure.phabricator.com/book/phabricator/article/arcanist/)) - * [Phabricatorにパッチを送付する](https://ghc.haskell.org/trac/ghc/wiki/Phabricator#Startingoff:Fixingabugsubmittingareview) [(例)](https://phabricator.haskell.org/D4235) - * 具体的なコマンドは"arc diff HEAD~"。 最後のcommitが送信される。 - * Tracのticketの、"Differential Rev"の箇所にPhabの管理番号を書いておく [(例)](https://ghc.haskell.org/trac/ghc/ticket/14473) - * Phabricator上で、コードレビューしてもらう(待つ、議論する) - * 必要に応じてコードを修正する - * コード修正後に、修正パッチを送り直すコマンドは"arc diff"。 - * レビュー待ちの間に、masterとconflictを起こした場合は、パッチを送り直すと親切。 - * レビュー待ちの間に、masterとの差分が大きくなった場合は、"git rebase"してから送り直すのも親切。rebaseについては[ここを参照](https://ghc.haskell.org/trac/ghc/wiki/Phabricator#Workingwithmultipledependentdiffs) - * レビューが完了してmasterブランチに取り込まれたら、proposalsの"implemented"のフィールドに、実装済みのGHCのバージョン番号を記載しておく [(例)](https://github.com/ghc-proposals/ghc-proposals/commit/1974c2a45a782461084ea596ec839638d4ff0743#diff-ffb9f607b8f1e69494a276ae9afa8268) - - -### パッチ送付の例 - -数値リテラルの構文を変更する場合の、具体的なパッチ送付の例を紹介します。 - -* [https://phabricator.haskell.org/D4235](https://phabricator.haskell.org/D4235) - -その他のレビュー中パッチの例は以下にたくさんあります。 - -* [https://phabricator.haskell.org/differential/](https://phabricator.haskell.org/differential/) - - - -### いくつかのポイントなど - -* 他の良いパッチが参考になります(同じ種類の修正を探すと、修正方法や慣習や修正漏れなどを確認できます。) -* build確認とvalidation確認は絶対に行う(つたないコードは問題視されませんが、本来行うべき手順を行わないことは、開発全体にダメージを与えるとともに、個人の信用度に影響します。) -* 数カ月単位で気長に根気よくやる(パッチ作業は多数並走しており、GHCのリリース時期は特に多忙です。全員がボランティアで行っている自発的な活動ですので、忘れられている状況へのpingは構いませんが、強い催促は控えるのが賢明です。) -* わからない点は、ghc-devs MLやPhabricator上で相談するとよいでしょう。 -* Phabricator(arcコマンド)には慣れが必要かと思います。最初は影響範囲の少ない、ドキュメント修正などでPhabricatorの作業手順に慣れていくのも良いです。 - -パッチ送付は、Phabricatorやgitの機能を用いて行うものです。操作ミスがあったところで、GHCのリポジトリ本体に直ちに反映されるわけではありません。やり直しは何度でも行えます。失敗やミスを不必要に怖れる必要はありません。communityのためになるcontributionは常に歓迎されています。 - -それでは、パッチ送付プロセスをお楽しみ! - - - ------- - -## 補足 - -わからないことがあれば、[ghc-devsのML](https://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs)に問い合わせると親切に教えてもらえます。 -もちろん、[Haskell-jpのslack](https://haskell.jp/signin-slack.html)の#questionsチャネルなどで尋ねるのも良いでしょう。 - -なお、GHCでの開発作業については、[Working on GHC](https://ghc.haskell.org/trac/ghc/wiki/WorkingConventions)も参考にどうぞ。 -また、GHCの開発フロー全体については、[こちら](https://takenobu-hs.github.io/downloads/ghc_development_flow.pdf)も参考にどうぞ。GHC関連のサイトの情報を力づくで検索するには、[こちら](https://takenobu-hs.github.io/haskell-wiki-search/)もどうぞ。 - - - -Happy Hacking! - -以上です。 - diff --git a/preprocessed-site/posts/2018/haskell-day-2018.md b/preprocessed-site/posts/2018/haskell-day-2018.md deleted file mode 100644 index b86f8369..00000000 --- a/preprocessed-site/posts/2018/haskell-day-2018.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: Haskell Day 2018 開催レポート -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -author: Kazuki Okamoto -postedBy: Kazuki Okamoto (@kakkun61) -date: November 20, 2018 -tags: イベント ---- - -こんにちはkakkun61こと岡本和樹です。 - -去る11月10日にHaskell Day 2018が開催されましたので、そのイベントレポートをお送りします。 - -# Haskell Day 2018とは - -学んだことをまとめると
— Yuji Yamamoto: 山本悠滋 (@igrep) 2017年12月5日
- Invalid characterと言われたらchcp 65001しよう
- Permission Deniedと言われたらビルドし直そう
- 日本語のパスが混ざらないよう気をつけよう
- Cのライブラリーはものによる
ですか。多分 #haskell 以外でも有益な話。
- - -あと技術書典5に、鴨川書房というサークル名で合同本を出品します。HaskellによるNN実装(@lotz84_) や、FPGAでのauto encoder実装に関する苦労話等が掲載される予定です。ぜひに🙏……
— ✨🤩😝🤪パリピ🤭🤢🤮✨ (@chaoticCats) 2018年8月9日
- - -# か38 — だめぽラボ - -[カタログ](https://techbookfest.org/event/tbf05/circle/37190001) - -## 代数的数を作る 多項式の根と因数分解のアルゴリズム - -- 同人誌 -- 268ページ -- ¥2500 - -[ブログの告知記事](https://blog.miz-ar.info/2018/09/techbookfest5/) - -> 代数的数(整数係数多項式の根として表される数)を実装するためのアルゴリズムを解説します。代数的数を使うと、ルートを含むような数に関して、浮動小数点数の誤差に煩わされることなく正確な演算が行えます。Haskellによるサンプルコードを掲載しています。 -> -> この本は、Web連載していた「週刊 代数的実数を作る」 https://miz-ar.info/math/algebraic-real/ の書籍化です。本文の加筆修正の他、「付録A ユークリッドの互除法と拡張された互除法」「付録B 部分分数分解」を追加しています。 -> -> [カタログ](https://techbookfest.org/event/tbf05/circle/37190001)より - -形態素解析ライブラリnagisaについては,技術書典の合同本に寄稿してもらえるよう作成者に依頼中なので興味ある方は是非(表紙は鋭意作成中)。 pic.twitter.com/zUtngAS23t
— ✨🤩😝🤪パリピ🤭🤢🤮✨ (@chaoticCats) 2018年9月18日
- - -# か61 — 趣味はデバッグ…… - -私のサークルです。新刊落としました…… - -## 手続きHaskell - -[カタログ](https://techbookfest.org/event/tbf05/circle/45000003) - -- 同人誌 -- 28ページ -- ¥500 -- [http://doujin.kakkun61.com/procedural-haskell](http://doujin.kakkun61.com/procedural-haskell) - -> Haskellでの手続きプログラミングの側面について解説します。 -> -> 対象読者 -> - Haskell入門書程度が読める -> - 特に読めるが書こうとすると悩む人に読んでほしいです -> - 手続きプログラミングのプログラマー -> - 厳密に本書を読むためにはHaskellを読めた方がよいですが、手続きプログラミングですのでプログラマーなら雰囲気で読めると思います -> -> 書かれてあること -> - 書き換え可能な変数 -> - 手続きプログラミング的な制御構造 -> - 配列 -> - サンプルプログラム -> - 手続き的な実装とHaskell的な実装の対比 -> -> 電子版(PDF)はこちらで販売中です。 -> https://kakkun61.booth.pm/items/829369 -> -> [カタログ](https://techbookfest.org/event/tbf05/circle/45000003)より - -## Haskellで作るWebアプリケーション 遠回りして学ぶYesod入門 - -- 商業誌 -- 76ページ -- ¥1500 -- [https://nextpublishing.jp/book/9979.html](https://nextpublishing.jp/book/9979.html) - -> 【HaskellのウェブアプリケーションフレームワークYesodの入門書!】 -> -> 本書は、Haskellの入門書レベルの知識をもつ読者を対象とした、ウェブアプリケーションフレームワークYesodの入門書です。比較的学習コストの高いYesodですが、本書を通じてYesodの基本的な知識とHaskellでのウェブアプリケーション開発に挑んで見ましょう! -> 〈本書の対象読者〉 -> Haskellの入門書は既に読みこなしているプログラマ -> Haskellでウェブアプリを作ってみたいプログラマ -> -> [出版社ページ](https://nextpublishing.jp/book/9979.html)より - -ちなみにこんな本を作るつもりでした。欲しい方いらっしゃったら次で書けとお伝えください。はげみになります。 - -技術書典5 か38で「代数的数を作る 多項式の根と因数分解のアルゴリズム」を頒布します。よろしくお願いします。 https://t.co/HkLF1YFDuN pic.twitter.com/V17ZIj2Iub
— だめぽラボ@技術書典5 か38 (@mod_poppo) 2018年9月29日
- - -# か74 — 大宇宙銀河No.1-Haskeller-にこにー - -[カタログ](https://techbookfest.org/event/tbf05/circle/43260001) - -## 矢澤にこ先輩といっしょに代数! - -- 同人誌 -- 84ページ -- ¥1000 - -[ブログの告知記事](http://aiya000.github.io/posts/2018-09-12-techbookfest5.html) - -> ゆるふわにこまき数学! -> -> 以下のような人に向けて、頒布します。 -> -> - 数学・代数の雰囲気をゆるく知りたい -> - 軽いHaskellを知りたい -> - なんでもいいから技術系にこまきが読みたい -> -> [カタログ](https://techbookfest.org/event/tbf05/circle/43260001)より - -技術書典5にサークル「趣味はデバッグ……」として参加申込をしました! | 技術書典 https://t.co/nD4eBo9622 「自作静的型付け言語を作ってそれに対して型推論する方法を解説する」書籍を作るぞ!
— kakkun61@技術書典5 か61 (@kakkun61) 2018年6月20日
- - -# それでは当日に - -当日は安全に配慮しつつ楽しんでいきましょう!! - -1000円札と500円玉の準備はしっかりとね。 diff --git a/preprocessed-site/posts/2018/topic-request.md b/preprocessed-site/posts/2018/topic-request.md deleted file mode 100644 index 8e20db3b..00000000 --- a/preprocessed-site/posts/2018/topic-request.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Haskell-jp Blogで書いてほしいネタを募集します! -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -subHeading: -author: Haskell-jp -tags: Haskell-jp -postedBy: Haskell-jp -date: February 18, 2018 -... ---- - -Haskell-jp Blogでは、設立当初よりHaskellに関する記事を幅広く募集してきました。 -このたびはそれに加え、このHaskell-jp Blogで「書いて欲しい!」「読んでみたい!」Haskellに関する話題も募集することにしました! -例えば、下記のような話題が考えられるでしょう。 - -- ○○パッケージの解説記事が欲しい -- 数学用語とHaskell用語の対応関係が知りたい -- Real World Haskellを今読むならの注意点 -- そのほか、Haskellに関する話題であれば何でも! - -**ただし、提案していただいたネタに関する知見の持ち主が居ないかもしれませんし、誰かの負担になるものなので必ず記事になるとは限りません。** -また、場合によっては既に記事があるため、既存の記事を薦められるかもしれません。 - -## ネタを提案してくれる場合 - -[このBlogのリポジトリのIssue](https://github.com/haskell-jp/blog/issues/new?template=topic-request.md&labels=Topic+Request)からお願いします。 -既に提案されていたら、そのIssueに対して 👍 するといいと思います。 - -## 書いてもいいよって場合 - -`TopicRequest` というラベルを作ったので、[Issueをそのラベルで検索](https://github.com/haskell-jp/blog/issues?q=is:issue+is:Aopen+label:"Topic+Request")してください。 -書いてもいいという提案があった場合は、Issueに「書いてもいいよ」という旨をコメントしていただけるとバッティングが無くて助かります。 - - -それでは、今後はHaskellに関するあなたの記事だけでなく、あなたがHaskellについて読みたい記事も募集していきますので、どしどし応募してください! 🙏 diff --git a/preprocessed-site/posts/2018/unordered-containers-hash-dos.md b/preprocessed-site/posts/2018/unordered-containers-hash-dos.md deleted file mode 100644 index a39f5f10..00000000 --- a/preprocessed-site/posts/2018/unordered-containers-hash-dos.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: hashdos脆弱性とunordered-containers -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -subHeading: HashMap・HashSetの利用時は注意! -author: Yuji Yamamoto -postedBy: Yuji Yamamoto(@igrep) -date: January 21, 2018 -tags: Security -... ---- - -あらゆるソフトウェアに脆弱性は存在し得ます。 -Haskellは高度な型システムを駆使することで、脆弱性を根本的に回避したプログラムを作ることを可能にします(脆弱性を防ぐためだけのものではないですが、興味のある人は[Safe Haskell](http://www.kotha.net/ghcguide_ja/7.6.2/safe-haskell.html)についても調べてみるといいでしょう)。 -しかし、だからといって、型を設計する段階で脆弱性を回避できるよう気をつけなければいけないことには変わりませんし、GHCが生成した実行ファイル、使用するライブラリーに絶対に脆弱性がないとは言えません。 -現状、Haskellはほかの著名なプログラミング言語ほど使用されていないためか、あまり脆弱性が報告されることはありません(libcなど、ほかの言語の処理系も依存しているようなライブラリーの脆弱性は別として)。 -今回は、そんな中でも[unordered-containersというパッケージ](https://hackage.haskell.org/package/unordered-containers)について、[ドキュメントにも書かれている](https://github.com/tibbe/unordered-containers/blob/60ced060304840ed0bf368249ed6eb4e43d4cefc/docs/developer-guide.md#security)ため**おそらく直ることがないであろう脆弱性**と、その回避方法について紹介します。 -hashdos脆弱性自体は結構有名ですし、ドキュメントに書いてあることなので、ご存知の方には何を今更感があるかと思いますが、検索した限りこの問題について日本語で説明した記事は見当たらなかったので、ここで紹介します。 - -# そもそもunordered-containersって? - -脆弱性の前にunordered-containersパッケージについて簡単に紹介しましょう。 -[unordered-containersパッケージ](https://hackage.haskell.org/package/unordered-containers)は、GHCに標準で付いている[containersパッケージ](https://hackage.haskell.org/package/containers)よりも高速な連想配列([`HashMap`型](https://hackage.haskell.org/package/unordered-containers-0.2.8.0/docs/Data-HashMap-Lazy.html))や集合([`HashSet`型](https://hackage.haskell.org/package/unordered-containers-0.2.8.0/docs/Data-HashSet.html))を提供してくれます。 -[StackageのLTS Haskell 10.3ではなんと970ものパッケージに依存されている](https://www.stackage.org/lts-10.3/package/unordered-containers-0.2.8.0)、超大人気汎用パッケージです。 - -## どうやって高速化しているの? - -`HashMap`という名前が示しているとおり、キーとなる値のハッシュ値を計算・利用することで高速化しています。 -しかし、Java言語などほかの言語によくある`HashMap`とは大きく異なり、内部ではハッシュテーブルを使用していません。 -[本物のプログラマはHaskellを使う - 第35回 キーを使って値を参照するMap型:ITpro](http://itpro.nikkeibp.co.jp/article/COLUMN/20091104/340002/?rt=nocnt)でも説明しているとおり、ハッシュテーブルはミュータブルな配列を内部で使用していることから、イミュータブルなデータ構造を使用して行う関数型プログラミングとは、相性が悪いのです(`ST`モナドや`IO`モナドを利用した[hashtablesパッケージ](https://hackage.haskell.org/package/hashtables)などを使えば、限られた範囲内でハッシュテーブルを使うこともできます)。 - -ハッシュテーブルを使用しない代わりに、unordered-containersでは内部で[Hash array mapped trie](https://en.wikipedia.org/wiki/Hash_array_mapped_trie)という特殊な木を使っています。 -どのような構造かは、[HAMT ~ イミュータブルで高速なハッシュマップ ~ | κeenのHappy Hacκing Blog](http://keens.github.io/slide/HAMT/)に詳しく書かれています。 -こちらのスライドはScalaでの実装の話ですが、基本的にはunordered-containersパッケージの`HashMap`も同じはずです。 - -大雑把に言うと、Hash array mapped trieを使った`HashMap`では、ハッシュテーブルと同様に、キーとなる値を**ハッシュ関数で一旦固定長の整数に変換する**ことで、キーが存在しているかどうかの確認を高速化しています。そのため、containersパッケージよりも高速な処理ができるのです。 -containersパッケージの`Map`ではキーの存在を確認する際、キー全体を既存のキーと比較する必要があるため、特に長い文字列をキーとする場合は、処理が遅くなりがちだったのです。 - -# hashdos脆弱性とは? - -hashdos脆弱性は[2011年頃RubyやPHP、Perlなど多くのプログラミング言語が影響を受けるとされた](https://blog.tokumaru.org/2011/12/webdoshashdos.html)、著名な脆弱性です。 -ここでも簡単に仕組みを説明しましょう。 - -前節で説明したとおり、Hash array mapped trieもハッシュテーブルも、必ずキーを一旦固定長の整数に変換します。 -文字列など、ハッシュ関数を適用されるキーとなる値は、当然固定長の整数よりも幅広い値を取り得るので、違う文字列同士でも、同じハッシュ値をとることがあります。 -この、違う値であるはずのキーが同じハッシュ値をとってしまった状態を「ハッシュ値の衝突」と呼びます。 -ハッシュ値の衝突が発生した場合、ハッシュテーブルやHash array mapped trieといったハッシュ値を利用した連想配列は、(単純な)配列やリストなど、やむを得ず逐次探索が必要なデータ構造を内部で使用しなければならなくなります。 - -hashdos脆弱性はこの性質を利用したDoS攻撃です。 -攻撃者は、あらかじめ対象のプログラムで使っているハッシュ関数が、「必ず同じハッシュ値」を返すキー(大抵文字列でしょう)を大量に用意して、それを対象のプログラムに入力として与えることで、簡単にDoS攻撃を仕掛けることができるのです。 -[先ほど触れた徳丸先生の記事](https://blog.tokumaru.org/2011/12/webdoshashdos.html)では、PHPのアプリケーションに対してわずか500KBのform-dataを送るだけでCPU時間を1分も消費させることができたそうですから、その威力はすさまじいものと言えるでしょう。 - -# なぜ直さないのか? - -[unordered-containersのDeveloper Guide](https://github.com/tibbe/unordered-containers/blob/60ced060304840ed0bf368249ed6eb4e43d4cefc/docs/developer-guide.md#security)には、次のように書かれています。 - -> There's an uncomfortable trade-off with regards to security threats posed by e.g. denial of service attacks. Always using more secure hash function, like SipHash, would provide security by default. However, those functions would make the performance of the data structures no better than that of ordered containers, which defeats the purpose of this package. - -要するに、「セキュリティー上問題はあるけど、SipHashのような安全なハッシュ関数を使ったらcontainersパッケージよりも速度が出なかった。それではこのパッケージの意味がない」ということです。 -containersパッケージよりも高速な連想配列を作るためにunordered-containersパッケージを作ったのだから、それより遅くなっては存在価値がなくなってしまうのです。 -従って、ユーザーが任意にキーを入力できるようなプログラムでは、unordered-containersではなく、containersを使え、ということです。 -このことはunordered-containersが使用している[hashableのドキュメント](https://hackage.haskell.org/package/hashable-1.2.6.1/docs/Data-Hashable.html#g:1)にも書かれています。ある意味ノーガード戦法ですね。 - -# 回避方法 - -前節で触れたとおりですが、**ユーザーが任意にキーを入力できるようなプログラム**では、unordered-containersパッケージの`HashMap`や`HashSet`ではなく、containersパッケージの`Map`や`Set`を使いましょう。 -containersパッケージにある`Map`や`Set`はハッシュ関数を一切使っていないので、ハッシュ値の衝突も起こらず、内部で逐次探索が必要なデータ構造を使ってもいません。 -なのでhashdos攻撃に遭うことはないのです。 - -ただし、実際のところ、[StackageのLTS Haskell 10.3で970ものパッケージに依存されている](https://www.stackage.org/lts-10.3/package/unordered-containers-0.2.8.0)unordered-containersです。 -~~その中にはJSONのパーサーであるaesonも含まれているので、もしかしたら現状回避するのは非常に困難なのかもしれません。😱~~ -~~次回は、この問題について試しに攻撃用のコードを書いて速度の低下をチェックして報告する話を書くかもしれません...。😰~~ - -**2022/10/23 追記**: aesonパッケージがHashDoS脆弱性を孕んでいるという問題は、[2021年9月に発表され](https://cs-syd.eu/posts/2021-09-11-json-vulnerability)、同脆弱性はaesonパッケージのv2.0.1.0で修正されました。現在は、コンパイル時のフラグを編集しない限り、内部でcontainersパッケージの`Map`を使うようになりました。ただし、[フラグのデフォルト値は将来変更するかも知れない、と開発者が明言している](https://github.com/haskell/aeson/issues/864#issuecomment-939363297)ので、心配な方はcabal.projectやstack.yamlでフラグの値を指定しましょう([参考](https://frasertweedale.github.io/blog-fp/posts/2021-10-12-aeson-hash-flooding-protection.html#compiling-a-safe-version-of-aeson))。この記事を書いた時点で当然私もこの問題には気づいていたのですが、再現ケースの作成に失敗したこともあり、報告に至らなかったことを反省します。本件を発表した記事と同じ、[fnv-collider](https://github.com/Storyyeller/fnv-collider)を使ったはずなのになぁ😞。 diff --git a/preprocessed-site/posts/2018/windows-gotchas-en.md b/preprocessed-site/posts/2018/windows-gotchas-en.md deleted file mode 100644 index b8f9c8b5..00000000 --- a/preprocessed-site/posts/2018/windows-gotchas-en.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -title: Errors and the workarounds frequently encountered when dealing with Haskell on Windows -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -subHeading: Quick-and-dirty checklist -author: Yuji Yamamoto -postedBy: Yuji Yamamoto(@igrep) -date: May 25, 2018 -tags: Windows -language: en -... ---- - -This is the English version of [WindowsでHaskellを扱う時によく遭遇するエラーと対処法](https://haskell.jp/blog/posts/2017/windows-gotchas.html). -The original article is the 4th article of [Haskell (その4) Advent Calendar 2017 (Japanese)](https://qiita.com/advent-calendar/2017/haskell4). - - -What I'm going to tell is summarized as [just one tweet (originally in Japanese)](https://twitter.com/igrep/status/938056578934042626): - -> What I've learned: -> -> - chcp65001 if 'Invalid character' -> - rebuild if 'Permission Denied' -> - Don't mix Japanese characters in file paths. -> - Some libraries in C are available, and others are not. -> -> Perhaps they're helpful in other languages. - -Let me add more details. - -# chcp 65001 if "Invalid character" - -You would have encountered this frequently, especially if you don't know how to avoid/fix this. -Oh, it's caused again by building with hakyll! - - -``` -> stack exec -- site rebuild -... - [ERROR] preprocessed-site\posts/2017/01-first.md: hGetContents: invalid argument (invalid byte sequence) -``` - -The object called [`Handle`](https://www.stackage.org/haddock/lts-10.0/base-4.10.1.0/System-IO.html#t:Handle), used by GHC to read and write a file, knows its character encoding. - - -This resembles Ruby's [`IO`](https://ruby-doc.org/core-2.5.0/IO.html) and Perl's file handler. -Both of them represent the "gateway" of data, and assigning character encoding to them enables us to handle the only, consistently encoded strings by converting the incoming data. -In Haskell's type `Char`, the only default encoding is UTF-32 (is this the right name in this case?). - - -The character encoding assigned to a `Handle` by default depends on the locale settings of the OS: in Japanese Windows, Windows-31J (a.k.a CP932). -But it's now soon becoming 2018 (when writing the original article). Most files you create should be in UTF-8 unless you write programs in notepad.exe[^notepad]. -It doesn't work to read a UTF-8 file as a Windows-31J file because they're very different encoding system. -The `invalid byte sequence` error, shown at the head of this section, is caused by that inconsistency. -Remember this kind of errors are often caused when reading or writing stdout/stdin, as well as plain files. - - -[^notepad]: Translator's note: In Japanese locale, notepad.exe saves the file in Windows-31J. This will be changed (into UTF-8) in the future release of Windows 10. - -## Workaround - -### If you encounter as a user - -In many cases you can avoid these kind of errors by running the below command in advance. - - -``` -> chcp 65001 -> stack exec -- site rebuild -... Should work! -``` - -This command temporarily changes the character encoding in the current Command Prompt session. -The number `65001` seems to stand for UTF-8. -To roll it back, run `chcp 932`. - - -``` -> chcp 932 -``` - -It seems that the "932" of "CP932" is the same "932" entered here! - - -The `chcp` command is available in MSYS2's bash (Surprises me a little. Wondering how it works...😕). -But you should know that `chcp` exists at `C:\Windows\System32\`, which MSYS2 users usually don't want to include in the `PATH`. -The directory contains many incompatible commands whose names conflict with the tools loved by Unix people (e.g. `find.exe`)! - - -So I've dropped `C:\Windows\System32\` from `PATH` when using MSYS2. -If you've done like me, run by full path: - - -``` -/c/Windows/System32/chcp.com 932 -``` - -### If it still doesn't work, or you're the developer of the libraries etc. - -Unfortunately, the error can often persist even after running `chcp 65001`[^eta-20127]. -According to my guess, the `chcp 65001` command doesn't affect the grandchild processes of the Command Prompt (or bash etc.) on which the `chcp` is run (i.e. the child processes of the command you enter). - -[^eta-20127]: By the way, when I once tried to build the compiler of [Eta](http://eta-lang.org/), (as far as I remember) `chcp 65001` didn't fix the problem, but `chcp 20127` did. -As `chcp 20127` switches into US-ASCII, I suspect the local environment of the developer of Eta is US-ASCII... - -If the error still happens you can either report to the developer, or fix it yourself! -When reporting; asking the developer to run after doing `chcp 932` could help him/her reproduce the bug (Sorry, I've never tried it). -When fixing by yourself, perhaps the best and most certain way would be to switch the character encoding of the `Handle` object. - - -This problem is caused by the inconsistency between the `Handle`\'s character encoding and the encoding of the bytes that are actually transferred. So switching into the proper encoding should fix it. -If the error happens when reading/writing a common UTF-8 file via the `Handle`, writing like below can avoid it: - - -```haskell -import System.IO (hSetEncoding) -import GHC.IO.Encoding (utf8) - -hSetEncoding handle utf8 -``` - -As a bonus, I'll show you an example of how [I myself addressed a problem caused by the standard output (or standard error output), and fixed a bug in haddock](https://github.com/haskell/haddock/pull/566). -In short, it can at least suppress the error to paste the code below before your program uses the `Handle` (Copied from [this commit](https://github.com/haskell/haddock/pull/566/commits/855118ee45e323fd9b2ee32103c7ba3eb1fbe4f2)). - - -```haskell -{-# LANGUAGE CPP #-} - -import System.IO (hSetEncoding, stdout) - -#if defined(mingw32_HOST_OS) -import GHC.IO.Encoding.CodePage (mkLocaleEncoding) -import GHC.IO.Encoding.Failure (CodingFailureMode(TransliterateCodingFailure)) -#endif - -... - -#if defined(mingw32_HOST_OS) - liftIO $ hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure -#endif -``` - -CPP macros to `import` modules only available on Windows makes this code hard to read, so let's cut out the verbose part: - - -``` -hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure -``` - - -Here're the details: -First of all, `hSetEncoding` is the function to change the `Handle`'s character encoding, as I referred before. -Then `stdout` is the `Handle` for the standard output as its name. -The last function call `mkLocaleEncoding TransliterateCodingFailure` returns a character encoding object for the current Windows' character encoding (i.e. `chcp`ed character encoding), configured as "Even if the `Handle` detects any characters which can't be converted into/from a Unicode character, don't raise an error, convert it into some likable character instead.". - -As the result of the `hSetEncoding` above, and the current character encoding is Windows-31J, the character used in the compilation error of GHC: - -``` -↓This character -• No instance for (Transformation Nagisa CardCommune_Mepple) -↑ -``` - -is converted into - - -``` -? No instance for (Transformation Nagisa CardCommune_Mepple) -``` - -the question mark. Yeah, this is the "?" I bet most users of GHC on Japanese Windows have seen at least once 😅 -This makes me guess GHC executes `hSetEncoding stderr $ mkLocaleEncoding TransliterateCodingFailure` by default before printing out the compilation error. -Anyway, it's good that the program doesn't abort due to the error! - - -As the last note of this section: Read [the document of GHC.IO.Encoding](https://hackage.haskell.org/package/base-4.10.1.0/docs/GHC-IO-Encoding.html) for the details of how GHC handles various character encodings. - -# Rebuild if "Permission Denied" - -I've made the first section too long for "Quick-and-dirty checklist", but I'll tell you in short from this section. -We often encounter some errors like "Permission Denied", "Directory not empty" and similar ones when running `stack build`, `ghc`, `elm-make`, and any other commands written in Haskell. -To tell the truth, I'm completely not sure of the cause, but those errors disappear by running the same command several times. -The key is to repeat many times. Never give up only by once or twice 😅 -Turning off your antivirus software's scanning of the problematic directory, Dropbox's synchronisation, etc. might also fix such errors. - - -# Try hard to build libraries in C... - -On Windows, it frequently troubles us to install libraries which depend on libraries written in C (registered as `lib***` in your OS's package manager). -But this is not the case only for Haskell. - - -The way to fix depends on the case, so let me give you some examples as external links (Sorry, all pages are written in Japanese!). - - -- HDBC-sqlite3: - - [Windows版stackでもHDBC-sqlite3をビルドする - Qiita](https://qiita.com/igrep/items/d947ab871eb5b20b57e4) - - [MSYS2でHDBC-sqlite3をコンパイル - 北海道苫小牧市出身の初老PGが書くブログ](http://hiratara.hatenadiary.jp/entry/2017/01/29/110100) -- [Haskell - Haskellにてstackでiconvパッケージを利用する方法【Windows環境】(102462)|teratail](https://teratail.com/questions/102462) - -That's all! -Then, Happy Hacking in Haskell on Windows 10!! I don't know WSL!🏁🏁🏁 diff --git a/preprocessed-site/posts/2018/windows-long-path.md b/preprocessed-site/posts/2018/windows-long-path.md deleted file mode 100644 index c323a5fd..00000000 --- a/preprocessed-site/posts/2018/windows-long-path.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: WindowsでHaskellを扱う時によく遭遇するNo such file or directoryについて -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -subHeading: 短いパスにしよう -author: Yuji Yamamoto -postedBy: Yuji Yamamoto(@igrep) -date: March 13, 2018 -tags: -... ---- - -去年、[WindowsでHaskellを扱う時によく遭遇するエラーと対処法](/posts/2017/windows-gotchas.html)という記事で、WindowsユーザーがHaskellで開発したとき、あるいはHaskell製のプログラムを使用した際によく遭遇するエラーやその回避方法を紹介しました。 -今回は、そこに追記したい内容として、最近私がよく出遭うようになったエラーを紹介します。 - -# `openFile: does not exist (No such file or directory)`といわれたら短いパスに移そう - -`does not exist (No such file or directory)`というエラーは、本当に読んで字のごとく、開こうとしたファイルが存在しないためのエラーであることとがもちろん多いのですが、エラーメッセージに反して違う原因である場合もあります。 - -例えば、最近私はとあるプロジェクトを数文字長い名前にリネームしたのですが、たったそれだけで、`stack test`した際必ず問題のエラーが発生するようになってしまいました。 - -``` -$ stack test -a-little-longer-name-project-0.1.0.0: build (lib + exe + test) -Preprocessing library for a-little-longer-name-project-0.1.0.0.. -Building library for a-little-longer-name-project-0.1.0.0.. -Preprocessing executable 'mmlh' for a-little-longer-name-project-0.1.0.0.. -Building executable 'mmlh' for a-little-longer-name-project-0.1.0.0.. -Preprocessing test suite 'a-little-longer-name-project-test' for a-little-longer-name-project-0.1.0.0.. -Building test suite 'a-little-longer-name-project-test' for a-little-longer-name-project-0.1.0.0.. -[1 of 5] Compiling Paths_aLittleLongerNameProject ( .stack-work\dist\5c8418a7\build\a-little-longer-name-project-test\autogen\Paths_aLittleLongerNameProject.hs, .stack-work\dist\5c8418a7\build\a-little-longer-name-project-test\a-little-longer-name-project-test-tmp\Paths_aLittleLongerNameProject.o ) -.stack-work\dist\5c8418a7\build\a-little-longer-name-project-test\a-little-longer-name-project-test-tmp\.stack-work\dist\5c8418a7\build\a-little-longer-name-project-test\autogen\Paths_aLittleLongerNameProject.dump-hi: openFile: does not exist (No such file or directory) -``` - -どういうことかと悩んでいたところ、[こんなIssue](https://github.com/commercialhaskell/stack/issues/3649)を見つけました。 -[Snoymanの指摘](https://github.com/commercialhaskell/stack/issues/3649#issuecomment-351612621)のとおり、こちらの問題はWindowsで使えるパスの長さが原因のエラーのようです。 -どういうことかというと、[MSDNのこちらのページ](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%20\(v=vs.85\)#maxpath)でも触れているとおり、Windowsの(C言語レベルでの)各種ファイル操作用APIでは、一度に扱えるパスの長さが260文字までと決められていて、その制限にかかったためのエラーだというのです! -`does not exist (No such file or directory)`なんてエラーメッセージで表されるのでわかりづらい!(おそらくWindowsのエラーコードの出し方に問題があるんじゃないかと思います) - -DOS時代から残るこの制限、完全に時代錯誤なものでしかないのですが、Windowsでパッケージマネージャーなどが自動的に作ったパスを扱っていると、しばしば出くわすことがあります。 -stackにおいても、[こちらのIssue](https://github.com/commercialhaskell/stack/issues/3285)で同じ問題が議論されていたり、[ver. 1.6.5のChangeLog](https://github.com/commercialhaskell/stack/releases/tag/v1.6.5)でも言及されていたりと、至る所で格闘している跡があります。 - -## 回避方法 - -そんな`does not exist (No such file or directory)`ですが、残念ながら私が知る限り、プロジェクトなどのパスを(`C:\`などのよりルートに近い場所に置いて)より短くする以外の回避方法はありません。 -[haskell-ide-engineのインストール方法のページ](https://github.com/haskell/haskell-ide-engine#installation-on-windows)曰く、(新しめの)Windows 10であれば、グループポリシーを編集して、「Win32の長いパスを有効にする」を「有効」にすれば回避できるとのことですが、残念ながら手元で試した限りうまくいきませんでした。何かやり方がまずかったのかもしれませんが。 -いずれにしても、`stack build`コマンドなどを実行したときに問題のエラーに遭遇した場合、ビルドしたいもののパスをなんとかして短くする以上の方法はありません。 -`C:\`直下をホームディレクトリのように使う人が今でもたくさんいるわけです。 - -一方、あなたが問題のエラーが発生するプログラムを**修正する**ことができる立場にある場合、次の方法で回避できるかもしれません。 - -### 長いパスをより短くするために、カレントディレクトリーを変更して、相対パスを短くする。 - -本件はあくまでも、Windowsの各種ファイル操作用APIの1回の呼び出しで渡せる長さの制限ですので、制限を超えてしまうような場合はパスを分割すればよいのです。 -[filepathパッケージの`splitFileName`関数](https://hackage.haskell.org/package/filepath-1.4.2/docs/System-FilePath-Posix.html#v:splitFileName)や[`splitPath`関数](https://hackage.haskell.org/package/filepath-1.4.2/docs/System-FilePath-Posix.html#v:splitPath)を駆使してパスを分割した上で、対象のファイルの親ディレクトリーまで[directoryパッケージの`setCurrentDirectory`関数](https://hackage.haskell.org/package/directory-1.3.2.1/docs/System-Directory.html#v:setCurrentDirectory)で移動すれば、制限に引っかからないはずです(時間の都合でこちらについては試すコードを用意しておりません。あしからず)。 - -残念ながらカレントディレクトリーはプロセス全体で共有される情報ですので、マルチスレッドなプログラムでは頭の痛い問題が出てきてしまいますが、一番確実に回避できる方法のはずです。 -マルチスレッドである場合を考慮したくない場合は、次に紹介する方法を検討するとよいでしょう。 - -### Win32 APIのユニコード版の関数に、`\\?\`というプレフィックスを着けた絶対パスを渡す。 - -ここまでに出てきた、「Windowsの各種ファイル操作用API」は、すべて「Win32 API」と呼ばれるWindows固有のAPI群の一部です。 -この「Win32 API」に含まれる関数の多くは、「ユニコード版」とそうでないものに分かれます(詳細は[Conventions for Function Prototypes (Windows)](https://msdn.microsoft.com/ja-jp/library/windows/desktop/dd317766\(v=vs.85\).aspx)をご覧ください)。 - -このうち、「ユニコード版」のAPIには、この制限を緩和する専用の機能が含まれています。 -先ほども触れた[MSDNのページ](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%20\(v=vs.85\)#maxpath)曰く、なんと`\\?\`という変な文字列を絶対パスの頭に着けると、最大約32,767文字のパスまで受け付けるようになるというのです! -なんともアドホックな感じのする解決方法ですが、Microsoftが言うんだから間違いありません。 -いずれにしても32,767文字という微妙な最大文字数ができてしまいますが、UTF-16での32,767文字なので、そう簡単に超えることはないでしょう。 -いちいち絶対パスに変えて変なプレフィックスを加えないといけないという面倒くささはありますが、いちいち分割して相対パスに変換するよりは簡単なはずですので、検討する価値があります。 - -この、`\\?\`機能を試す場合、下記のコードを適当なファイルに貼り付けて保存し、`stack runghc file.hs`などと実行してみてください (Thanks, @matsubara0507!)。 -`catch`関数を使って例外を捕捉している箇所では、実際にパスが長すぎるためにエラーが発生し、`catch`されているはずです。 - -```haskell -import Control.Exception (catch, IOException) -import Data.List (replicate) -import System.Directory - -main :: IO () -main = do - crDir <- getCurrentDirectory - let - path1 = mconcat $ replicate 20 "abcdefgh/" -- ok - path2 = mconcat $ replicate 30 "abcdefgh/" -- error - path3 = crDir ++ "/" ++ path2 -- error - path4 = "\\\\?\\" ++ path3 -- ok - - putStrLn $ "path1: " ++ show path1 - createDirectoryIfMissing True path1 - - putStrLn $ "path2: " ++ show path2 - createDirectoryIfMissing True path2 `catch` (\e -> putStrLn $ " " ++ show (e :: IOException)) - - putStrLn $ "path3: " ++ show path3 - createDirectoryIfMissing True path3 `catch` (\e -> putStrLn $ " " ++ show (e :: IOException)) - - putStrLn $ "path4: " ++ show path4 - createDirectoryIfMissing True path4 -``` - -# おわりに - -さて、またしてもWindows固有の面倒な問題を紹介することとなってしまいましたが、俗世の喜び(主にゲーム)と簡単にインストールできるGUIに慣らされてしまった私は、今後もWindowsを使い続けるつもりです。 -いろいろ困難は尽きませんがこれからもWindowsでHappy Haskell Lifeを!🏁🏁🏁 - -# 参考URL - -※本文中で言及していないもののみ - -- [プログラマ的にWindows 10 Anniversary Updateのうれしいところ - kkamegawa's weblog](http://kkamegawa.hatenablog.jp/entry/2016/07/27/220014) -- [Windows 10 "Enable NTFS long paths policy" option missing - Super User](https://superuser.com/questions/1119883/windows-10-enable-ntfs-long-paths-policy-option-missing) diff --git a/preprocessed-site/posts/2019/asterius.md b/preprocessed-site/posts/2019/asterius.md deleted file mode 100644 index b97bddb2..00000000 --- a/preprocessed-site/posts/2019/asterius.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: AsteriusでHaskellの関数をJSから呼べるようにしてみた(けど失敗)(拡大版) -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -author: Yuji Yamamoto -postedBy: Yuji Yamamoto(@igrep) -date: May 4, 2019 -tags: Asterius, WebAssembly -... ---- - -先日、[Emscripten & WebAssembly night !! #7](https://emsn.connpass.com/event/121028/)というイベントにて、[Asterius](https://tweag.github.io/asterius/)というHaskellをWebAssemblyにコンパイルするツールについて紹介いたしました。 -資料はこちら👇です。 - -[AsteriusでHaskellの関数をJSから呼べるようにしてみた(けど失敗)](https://the.igreque.info/slides/2019-04-19-asterius.html#(1)) - -本日は、スライドの英語で書いていた箇所を和訳しつつ、いろいろ捕捉してブログ記事の形で共有します。 - -# 🔍Asteriusとは何か - -冒頭でも触れたとおり、[Asterius](https://tweag.github.io/asterius/)はHaskellのソースを[WebAssembly](https://developer.mozilla.org/ja/docs/WebAssembly)にコンパイルするコンパイラーです。 -GHCのHEAD(開発中のバージョン)を都度フォークして、現在活発に開発中です。 -Template Haskellと、GHC標準におけるIOを行う関数(の大半)を除いた、すべての機能が利用できるようになっています。 -現状のWebAssemblyを実用する上で必要不可欠であろう、FFIもサポートされています。 -つまり、JavaScriptからWebAssemblyにコンパイルされたHaskellの関数を呼んだり、HaskellからJavaScriptの関数を呼ぶことができます! -何かしらのIO処理を行う場合は、基本的にこのFFIを使ってJavaScriptの関数を呼ぶことになります。 - -加えて、`ahc-cabal`という名前のコマンドで、cabalパッケージを利用することもできます。 -こちらは`cabal`コマンドの単純なラッパーです。`ahc-cabal new-build`などと実行すれば、外部のパッケージに依存したアプリケーションも、まとめてWebAssemblyにコンパイルできます。 -本格的に開発する上では欠かせないツールでしょう。 - -# 👍Asteriusのいいところ - -Asteriusは、"A linker which performs aggressive dead-code elimination, producing as small WebAssembly binary as possible."と謳っているとおり、GHCのランタイムを抱えているにしては、比較的小さいWASMファイルを生成するそうです。 -というわけで手元で試してみたところ、下記のような結果になりました。 - -- 空っぽのプログラム(`main = return ()`しかしないソース): - - 36KB(`.wasm`ファイルのみ)。なかなかいい感じですね。 - - 168KB(実行時に必要な`.mjs`ファイルを含めた合計)。未圧縮でこれなら確かに十分軽いでしょう。Webpackなどで結合・minifyするともっと軽くできますし。 -- 今回私が移植を試みたアプリ(詳細は後ほど): - - 1.9MB(`.wasm`ファイルのみ)。うーん、ちょっと苦しいような...😥。 - - 2.1MB(実行時に必要な`.mjs`ファイルを含めた合計)。`.mjs`ファイルの内容は特に変わりませんでした。 - -ちなみに、移植前の元のソースを含むアプリを、Linux 64bit向けのELFファイルとしてビルドして比較してみたところ、`.wasm`ファイルよりも少し小さいぐらいでした。 -詳細な内訳が気にはなりますが、今のソースですと大体これぐらいが限界なのかも知れません(でもWASMは現状32bitバイナリー相当のはずだし、もう少し小さくならないものか...)。 - -加えて、Asteriusを利用して開発すると、ほぼ最新のGHCの開発版が使える、というところも、新しもの好きなHaskellerをわくわくさせるところですね!(今回はあいにく新しい機能について調べる余裕もなかったので、特に恩恵は受けてませんが...😅) -Asteriusは、GHCをフォークしていくつかの機能を追加して作られているものです。 -しかし幸いオリジナルとの差分が十分に小さく、作者が定期的にrebaseすることができています。 -詳細な違いは[About the custom GHC fork](https://tweag.github.io/asterius/custom-ghc/)にまとまっています。近い将来GHC本体に取り込まれそうな修正ばかりではないかと。 - -それからこれは、ブラウザーでHaskellを動かすことができるという点でAsteriusの競合に当たる、GHCJSと比較した場合の話ですが、FFIを利用して、JavaScriptから**直接**Haskellを呼ぶことができるようになっているのも、優れた点と言えるでしょう。 -GHCJSは[こちらのドキュメント曰く](https://github.com/ghcjs/ghcjs/blob/3959a9321a2d3e2ad4b8d4c9cc436fcfece99237/doc/foreign-function-interface.md#calling-haskell-from-javascript)、JavaScriptからHaskellを呼ぶ機能は備えてはいるものの、簡単ではないためドキュメントも書かれておらず、推奨されていません。 -これでは状況によってはかなり使いづらいでしょう。 -今回私が試したように、コアとなる処理だけをHaskellの関数として書いて、それをJavaScriptから呼び出すということができないのです。 - -一方Asteriusでは、例えば👇のように書くことで、WASMがエクスポートする関数として、`func`をJavaScriptから呼べるようにすることができます! - -```hs -foreign export javascript "func" func :: Int -> Int -> Int -``` - -ただし、実際に今回試してみたところ、Asteriusではまだバグがあったので、この用途では依然使いにくいという状況ではありますが...(詳細は後で触れます)。 - -# 👎Asteriusのイマイチなところ - -Asteriusは、やっぱりまだまだ開発中で、バグが多いです。 -今回の目的もバグのために果たせませんでした😢。 - -先ほども触れたとおり、特に未完成なのが、IOとTemplate Haskellです。 -GHCなら使えるはずの`IO`な関数の多くが使えませんし、Template Haskellに至っては一切利用できません。 - -IOについては、現状、(`putStrLn`などのよく使われる)一部を除き、FFI(`foreign import javascript`)を使ってJavaScriptの関数経由でよばなけれなりません。 -これは、入出力関連のAPIを一切持たないという現状のWebAssemblyの事情を考えれば、致し方ない仕様だとも言えます。 -[WASI](https://github.com/WebAssembly/WASI)の策定によってこの辺の事情が変わるまでの間に、すべて`foreign import javascript`で賄うというのも、なかなか面倒なことでしょうし。 - -Template Haskellに関しては、現在[こちらのブランチ](https://github.com/tweag/asterius/pull/81)で開発中です。...と、思ったらこのPull request、Closeされてますね...。 -これに関して詳しい事情はわかりません。いずれにしても、Template Haskellを実装するには、コンパイル時にその場でHaskellを評価するためのインタープリターが別途必要だったりして、結構ハードルが高いのです。 - -加えて、RTS(この場合、コンパイルしたHaskellを動かすのに必要なWASMやJavaScriptファイル)が[`BigInt`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/BigInt)に依存している関係で、V8やSpiderMonkeyでないと動かない点もまだまだ、という感じです。 -ブラウザーで言うと、2019年5月3日時点でChromeか、FirefoxのBeta版以降でないと使用できません[^firefox-stable]。 - -[^firefox-stable]: [Can I use](https://caniuse.com/#feat=bigint)曰く安定版でも`about:config`を書き換えればすでに使えるとのことなんですが、なぜか手元のFirefox 安定版ではうまくいきませんでした。確かに`about:config`にそれらしき設定はあるものの、`true`にしても何も変わらず...😰。 -ついでに細かいことを言うと、Firefox Nightlyは`about:config`を書き換えなくても使え、Beta版では`about:config`を書き換えると使えました。 - -# ⚙️Asteriusの仕組み - -Asteriusのドキュメント「[IR types and transformation passes](https://tweag.github.io/asterius/ir/)」をざっくり要約してみると、Asteriusは以下のような流れで動くそうです。 -実際には`ahc-link`というコマンドがこれらの手順をまとめて実行するので、ユーザーの皆さんはあまり意識する必要はないでしょう。 - -1. [フロントエンドプラグイン](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/extending_ghc.html#frontend-plugins)という仕組みでラップしたGHC(のフォーク)を使い、GHCが生成したCmmという中間言語で書かれたコードを、`AsteriusModule`という独自のオブジェクトに変換します。 -1. `ahc-ld`という専用のリンカーで、WASM向けにリンクします。 -1. 最後に、`ahc-dist`というコマンドで、リンクしたモジュールを実行できる状態にします。 - - [binaryen](https://github.com/WebAssembly/binaryen)か、[wasm-toolkit](https://github.com/tweag/asterius/tree/master/wasm-toolkit)というHaskellでWASMを書く言語内DSLを利用して、`ahc-ld`がリンクしたモジュールを検証し、`.wasm`ファイルに変換して、 - - 実行時に必要なJavaScriptファイルをコピーして、 - - Haskellのソースにおける`main`関数を実行する、エントリーモジュールを作ります。 - あとはこれをHTMLファイルから` - - -# 実行した環境 - -- Windows 10 Pro 64bit ver. 1809 -- `stack --version`: Version 1.9.1, Git revision f9d0042c141660e1d38f797e1d426be4a99b2a3c (6168 commits) x86\_64 hpack-0.31.0 -- TidalCycles: [1.0.6](http://hackage.haskell.org/package/tidal-1.0.6) -- SuperCollider: 3.10.0, 64bit -- Atom: 1.34.0 -- Atomのtidalcyclesプラグイン: 0.14.0 - -# 各種依存パッケージのインストール - -冒頭に挙げた質問をされた方が参考にしたページ [TidalCyclesのインストール2018年版 - Qiita](https://qiita.com/yoppa/items/fe21d7136f8f3aafd55c) では、Chocolateyを使ったインストール方法を紹介していますが、この方法では、直接GHCのWindows向けtarballをダウンロードしてインストールしているようです。 -私が知る限り特にその方法でも問題はないはずなんですが、なぜか質問者が挙げたようなエラーが発生してしまいます。 -また、TidalCyclesが実行時に依存しているSuperColliderやSuperDirtといったソフトウェアを、別のChocolateyのパッケージに分けることなく、TidalCyclesのインストールスクリプトで直接インストールしているようです(詳細は[Chocolateyのパッケージ情報](https://chocolatey.org/packages/TidalCycles)に書かれたchocolateyinstall.ps1を参照されたし)。 -そのため、ChocolateyでTidalCyclesをインストールしようとすると、問題のあるGHCと、SuperColliderなどの依存パッケージを一緒にインストールしなければなりませんし、SuperColliderやSuperDirtだけをChocolateyでインストールすることもできません。 - -なので、ここは素直に[TidalCycles公式のWiki](https://tidalcycles.org/index.php/Windows_installation)に書かれた方法に従ってSuperColliderやSuperDirtをインストールしつつ、Haskell関連のものだけstackでインストールしようと思います。 - -## [TidalCycles公式のWiki](https://tidalcycles.org/index.php/Windows_installation)そのままの手順 - -**⚠️行く先々でWindowsのファイアウォールの警告が出るかと思います。適当に承認しちゃってください!⚠️** - -1. [SuperColliderを公式サイトからインストール](https://supercollider.github.io/download)します。 - 今回は「Windows」の箇所に書いてある「3.10.0, 64-bit (no SuperNova)」というリンクをクリックしてダウンロードされた実行ファイルでインストールしました。 -1. [Atom](https://atom.io/)も公式サイトからインストールしました。 - 後で触れますTidalCyclesの対話環境を、Atom上で呼び出すためのプラグインがあるためです。他のエディタ向けのプラグインもありますが、公式サイトで紹介していたのはAtomなので、一番これがサポートされているのでしょう。 -1. GitもPrerequisitesとして挙げられていますが、すでに私の環境に入っているので今回は特に何もしていません。なければ普通に[Git for Windows](https://gitforwindows.org/)を入れるのが無難かと思います。 -1. SuperDirtのインストール - 1. SuperColliderをスタートメニューから起動します。 - 1. ウィンドウの左側にある「Untitled」と書かれた箇所の下がSuperColliderのエディタになっているようです(色がわかりづらい!)。 - そこに`include("SuperDirt")`と書いて、「Shift + Enter」を押せば、SuperDirtのインストールが始まります。 - 1. 次のセクションでSuperDirtを起動する前に、**一旦SuperColliderを終了**させましょう。 -1. Atom向けtidalcyclesプラグインのインストール - - 面倒なので省略します。他のプラグインと変わらないはずなので適当に検索してください! - -## TidalCycles公式のWikiとは異なる手順 - -ここからはこの記事特有の手順です。 -最近のHaskell開発者は、[stack](https://docs.haskellstack.org/en/stable/README/)というツールを使って開発環境を整えることが多いですので、冒頭の予告通りここではstackを使います。 -ちなみに、現在はHaskell Platformにもstackが添付されていますが、Haskell Platformに含まれる、GHCはstackを使うことでも簡単にインストールできるため、stackのみをインストールすれば十分です。 -なお、stack自体のインストール方法については拙作の[「失敗しながら学ぶHaskell入門」のREADME](https://github.com/haskell-jp/makeMistakesToLearnHaskell#%E3%81%BE%E3%81%A0stack%E3%82%84haskell-platform%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%97%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF)をご覧ください。 -Windowsではインストーラーをダウンロードして実行するだけで十分でしょう。 - -stackのインストールが終わったら、次の手順を踏んでください。 - -### stackでのTidalCyclesのビルド - -stackでTidalCyclesのビルドをするには、`C:\sr\global-project\stack.yaml`というファイルを、下記でコメントしたように書き換えてください。 - -```yaml -# ... 省略 ... -packages: [] -resolver: lts-12.26 # <= ここを編集 - -extra-deps: # <= この行と、 -- hosc-0.17 # <= この行を追記 -``` - -簡単に編集した内容について解説させてください。 - -まず、`resolver:`で始まる行ですが、これは「LTS Haskell」という、パッケージの一覧のバージョンを指定するものです。 -「LTS Haskell」は、「確実にビルドできるバージョンのパッケージをまとめた一覧」です。 -LTS Haskellのメンテナーの方々は、毎日登録された大量のパッケージをまとめてビルド・テストしてみることで、実際に登録されたバージョンのパッケージのビルドとテストが成功することを確認しています。 -なので、このLTS Haskellに登録されているバージョンのパッケージを使う限りは、私たちは安心してビルドができると言うことです。 - -なぜLTS Haskellのバージョンを書き換えたのかというと、それは、LTS Haskellには実際にはパッケージの一覧だけでなく、それらをビルドできるGHCのバージョンも含まれているからです。 -したがって、LTS Haskellのバージョンを指定する、ということは、そのままインストールするGHCのバージョンも指定することになります[^lts-haskell]。 -実は特に今回の場合、インストールするGHCのバージョンを指定しなければ、ビルドできない可能性が高かったのです。 -現在の最新のLTS Haskellに登録されているGHCのバージョンは「8.6.3」ですが、残念ながらこのバージョンのGHCには、[Windows版のみにおいて深刻なバグ](https://ghc.haskell.org/trac/ghc/ticket/16057)があります。 -実際にTidalCyclesをビルドする際にこのバグに遭遇するかは確かめてませんが、内容からして遭遇する確率が高そうであるという点と、遭遇するとビルドができないという点を考慮して、念のため確実にビルドできるバージョンのGHCを指定しておきました。 - -[^lts-haskell]: どのバージョンのLTS HaskellでどのバージョンのGHCがインストールされるかは、LTS Haskellを管理している[「Stackage」というウェブサイトのトップページ](https://www.stackage.org/)にある、「Latest LTS per GHC version」というセクションをご覧ください。 - -そして、`extra-deps`という項目は、ビルドしようとしているパッケージ(今回の場合`tidal`パッケージ)が依存しているパッケージが、LTS Haskellに登録されていない場合に指定するものです。 -[tidalパッケージ ver. 1.0.6のパッケージ情報](http://hackage.haskell.org/package/tidal-1.0.6)を確認すると、確かにhoscというパッケージに依存していると書かれていますね! -残念ながらこのhoscパッケージは今回指定した、LTS Haskellのver. 12.26には登録されていないので、上記のとおり`extra-deps`に明記しておいてください。 - -`C:\sr\global-project\stack.yaml`の編集が終わったら、 - -```bash -stack build tidal -``` - -と実行しましょう。 -初回はGHCのインストールも含めて行われるので、結構時間がかかると思います。 - -ちなみに、`stack install tidal`と実行してもいいですが、stackの仕様上、特に結果は変わりません。 -`stack install`は、実行ファイルがついたパッケージをビルドして`PATH`にインストールするためのコマンドなので、`tidal`のように実行ファイルがないパッケージでは意味がありません。 - -### Atomのプラグインの設定 - -続いて、Atomのtidalcyclesプラグインの設定をしましょう。 -stackは使用するGHCを、前述のstack.yamlに書いたLTS Haskellのバージョンに応じて切り替える関係上、`PATH`の通ったところにGHCをインストールしません。 -そのため、Atomのtidalcyclesプラグインに、stackがインストールしたGHCを認識させるには、下記のように設定を書き換える必要があります。 - -1. Atomを起動し、「File」 -\> 「Settings」の順にメニューをクリックして、Atomの設定画面を開きます。 -1. 画面左側の「📦Packages」と書かれた箇所をクリックすると、インストールしたAtomのプラグインの一覧が表示されるはずです。 -1. 一覧から「tidalcycles」を探して、「⚙️Settings」をクリックします。 -1. 「Ghci Path」という設定項目があるので、それを`stack exec ghci`に書き換えてください。 - -# 使い方・動作確認 - -## TidalCyclesを起動する度に必要になる手順 - -※[公式サイトのこちらのページ](https://tidalcycles.org/index.php/Start_tidalcycles_and_superdirt_for_the_first_time)に対応しています。 - -1. SuperDirtの起動 - 1. SuperColliderをスタートメニューから起動します。 - 1. 先ほど`include("SuperDirt")`と入力した、SuperColliderのエディタに、今度は`SuperDirt.start`と入力して、同じく「Shift + Enter」しましょう。 - SuperDirtが起動します。 -1. Atom上でのTidalCyclesの起動 - 1. Atomを起動して、拡張子が`.tidal`なファイルを開くか作成します。 - 1. メニューを「Packages」 -\> 「TidalCycles」 -\> 「Boot TidalCycles」の順に選択してください。 - 1. 画面下部でGHCiが起動し、TidalCyclesの式を実行するのに必要なパッケージの`import`や、`import`では賄いきれない関数の定義などが自動的に行われます。 - - [BootTidal.hs](https://github.com/tidalcycles/Tidal/blob/master/BootTidal.hs)というファイルの中身をGHCiに貼り付けているみたいです。 -1. 動作確認のために、適当なTidalCyclesの式 --- 例えば公式サイトのWikiどおり`d1 $ sound "bd sn"` --- を入力して、入力した行にカーソルを置き、「Shift + Enter」を押しましょう。 - 1. 入力した式が画面下部で起動したGHCiに送信され、実行されます。うまくいっていれば音が鳴るはずです。 - 1. 停止させたいときは、`d1 silence`と入力して同じく「Shift + Enter」を押してください。 -1. より詳しいTidalCyclesの使い方は、[TidalCyclesのチュートリアル1 - Qiita](https://qiita.com/mk668a/items/6e8e0151817f484a526c)など、他の方が書いた記事を検索してみてください。 - -# ハマったこと - -## 「SuperDirtが見つからない!」という趣旨のエラーが出た - -正確なエラーメッセージは申し訳なくも忘れてしまったのですが、SuperCollider上で`SuperDirt.start`と入力した際、エラーになることがあります。 -この場合、SuperColliderを再起動するのを忘れている可能性がありますので、再起動してみてください。 -SuperDirtのインストールを終えた直後では、まだSuperDirtは利用できないのです。 - -## Atom上でTidalCyclesを起動した際、`parse error` - -先ほどの「Atom上でのTidalCyclesの起動」という手順で、`parse error (possibly incorrect indentation or mismatched brackets)`というエラーに出遭うことがあります。 -そのままTidalCyclesの式を入力して「Shift + Enter」しても、`Variable not in scope: d1 :: ControlPattern -> t`などというエラーになってしまうでしょう。 -これは、前のセクションで触れたBootTidal.hsというファイルをGHCiが読み込む際に、エラーになってしまったからです。 - -原因はいろいろあり得るかと思いますが、私の場合、`~/.ghci`というGHCiの設定ファイルに`:set +m`という行を加えていたためでした。 -まず、`~/.ghci`は、GHCiが起動するときに必ず読み込まれるファイルです。 -必ず有効にしたい言語拡張や、`:set +m`のようなGHCiの設定を記載しておくファイルとなっています。要するに`~/.vimrc`などと似たようなものですね。 -そして`:set +m`は、GHCiで複数行の入力を有効にするためのものです。 -GHCi上で`:set +m`と実行すると、GHCiは入力した行を見て「あっ、この入力はまだ続きがありそうだな」と判断したとき、次の行を自動で前の行の続きとして扱うようになります。 -そして、その場合入力の終了をGHCiに伝えたい場合は、空行を入力しなければなりません。 -結果、BootTidal.hsを読み込む際に、空行が入力されないため、意図しない行が「前の行の続き」とGHCiに認識されてしまい、`parse error (possibly incorrect indentation or mismatched brackets)`となってしまうようです。 - -仕方ないので、直すために`~/.ghci`を開いて`:set +m`と書いた行をコメントアウトするか削除しちゃいましょう。 -再びAtomで「Packages」 -\> 「TidalCycles」 -\> 「Boot TidalCycles」の順にメニューをクリックすれば、今度は該当のエラーがなく起動するかと思います😌。 - -このエラーは、特にすでにHaskellの開発環境を導入している方で遭遇するケースが多いかと思います。ご注意ください。 - -## SuperDirtを起動し忘れていても何もエラーが起きない - -表題の通りです。 -困ったことにSuperDirtを起動し忘れた状態で`d1 $ sound "bd sn"`などの式を実行しても、特に何のエラーもなく、音も鳴りません。 -(サーバーとして起動しているべき)SuperDirtに接続し損ねたんだから、何かしらエラーが表示されてもいいはずなんですが、困ったことにウンともスンとも言いません😰。 -と、言うわけで、何のエラーもなく音も出なかった場合は、SuperDirtを起動し忘れてないか確認しましょう。 - -# おわりに: Haskell開発者として見たTidalCycles - -**※ここから先はおまけ + 宣伝です。TidalCyclesをインストールしたいだけの方は適当に読み飛ばしてください** - -ここまで、stackという、昨今のHaskellerの多くが好んで利用するツールで、TidalCyclesを利用する方法を説明しました。 -TidalCyclesの公式サイトのWikiにはこの方法は書かれてませんが、より確実なインストール方法として、覚えておいていただけると幸いです。 -すでにHaskellの開発環境をインストールしている方にも参考になるかと思います。 - -ところで、ここまでTidalCyclesを自分でインストールしてみて、Haskellerとしていくつか気になった点があります。 -TidalCyclesは、Haskell製の内部DSLとしては、ちょっと変わっているように感じました。 - -それは、TidalCyclesが「標準」として提供している関数を実行する際、tidalパッケージに含まれるモジュールを`import`するだけでなく、BootTidal.hsというファイルを読んで、追加の関数を定義する必要がある、という点です。 -大抵のHaskell製の内部DSLは、そんなことしなくてもモジュールを`import`するだけで使えるようになっています([Hspec](http://hspec.github.io/)とか[lucid](https://github.com/chrisdone/lucid)とか[clay](http://hackage.haskell.org/package/clay)とか[relational-record](http://khibino.github.io/haskell-relational-record/)とか)。 -つまり本来ならばわざわざ、BootTidal.hsのような、GHCiが読み込む専用のファイルを用意しなくとも良いはずなのです。 -このBootTidal.hsはAtomのプラグインの設定で簡単に切り替えることができるものなので、もし間違ったファイルに設定してしまったら、言語の標準にあたる関数がおかしな動作をすることになりかねませんし、あまり良いやり方だとは思えません。本来なら設定に混ぜて書くべきものではないでしょう。 - -なぜTidalCyclesはこんな仕様になっているかというと、それにはある意味Haskellらしい制約が絡んでいると推測されます。 -Atom上でTidalCyclesを起動する、というのは、実際にはGHCiを起動して、[BootTidal.hs](https://github.com/tidalcycles/Tidal/blob/master/BootTidal.hs)というファイルを読み込ませる、ということなのでした(事実、Atomなどのエディターを介さなくとも、お使いのターミナルエミュレーターから`ghci`コマンドを起動してBootTidal.hsファイルの中身をコピペするだけで、TidalCyclesは利用できます)。 -そのBootTidal.hsの中身を見てみると、サンプルで実行した`d1`という関数が、下記のように定義されていることがわかります。 - -```haskell --- ... 省略 ... -import Sound.Tidal.Context - --- total latency = oLatency + cFrameTimespan -tidal <- startTidal (superdirtTarget {oLatency = 0.1, oAddress = "127.0.0.1", oPort = 57120}) (defaultConfig {cFrameTimespan = 1/20}) - -let p = streamReplace tidal - --- ... 省略 ... - -let d1 = p 1 -let d2 = p 2 -let d3 = p 3 --- ... -``` - -`tidal <- startTidal`で始まる行で、TidalCyclesの初期化を行っていると思われます。 -初期化の際には、サーバーとして起動しているSuperDirtへの接続設定(この場合`127.0.0.1`の`57120`番ポートへ接続している)を渡しているようです。 -恐らくこの`startTidal`関数が、SuperDirtへ接続し、代入した`tidal`という変数に、SuperDirtへの接続を含んでいるんでしょう。 -そして、`let p = streamReplace tidal`という行で、その`tidal`を`streamReplace`関数に[部分適用](http://capm-network.com/?tag=Haskell-%E9%83%A8%E5%88%86%E9%81%A9%E7%94%A8)することで、`p`がSuperDirtへの接続を参照できるようにしています。 -さらに、`let d1 = p 1`などの行で、前の行で定義した`p`に整数(シンセサイザーの番号だそうです)を部分適用することで、結果、`d1`、`d2`などの関数へ、間接的に`tidal`を渡すことになります。 - -つまり`d1`や`d2`などの関数は、何らかの形で、SuperDirtへの接続情報を持っているのです。 -DSLとして、`d1`や`d2`などの関数に毎回接続情報を渡すのは煩雑だと考えたためでしょう。 -残念ながら、通常のHaskellがそうであるように、外部のサーバーに接続した結果取得されるものを、関数が暗黙に参照できるようにしたい場合、 --- つまり、今回のようにユーザーが接続情報を明示的に渡すことなく使えるようにしたい場合 --- 少なくともパッケージを`import`するだけではうまくいきません[^TemplateHaskell]。 -BootTidal.hsのように、SuperDirtのような外部に接続する処理を、GHCiの実行時に書かなければならないのです。 - -[^TemplateHaskell]: 後で軽く触れる、Template Haskellという邪悪なテクニックを使わない限りは。 - -しかし、`tidal <- startTidal`の行で作られるSuperDirtへの接続情報を`d1`などの関数が暗黙に利用できるようにすることは、実際にはBootTidal.hsで行っているような方法を使わなくともできます。 -そうすることで、BootTidal.hsを変なファイルに切り替えてしまって、`d1`などの関数の定義が間違ったものになってしまう(あるいはそもそも定義されなくなってしまう)リスクを回避できます。 -具体的には、下記のような方法が考えられます。 -申し訳なくも私はこれ以上TidalCyclesに入れ込むつもりもないので、誰かTidalCyclesを気に入った方が適当に提案するなりパッチを送るなりしてみてください(他力本願😰)。 - -- GHCiの中で`ReaderT`を使う - - Haskellで「関数に渡した引数を暗黙に利用できるようにする」といえば、やはり`ReaderT`モナドトランスフォーマーが一番オーソドックスな方法でしょう。 - 実はGHCi上では、`IO`以外のモナドのアクションで`print`することができます。 - [You can override the monad that GHCi uses](https://www.reddit.com/r/haskell/comments/87otrn/you_can_override_the_monad_that_ghci_uses/)というRedditのスレッドでは、`ReaderT`を使ったサンプルが紹介されています。 - これと同じ要領で、GHCiの`-interactive-print`というオプションに、`tidal`を`ReaderT`経由で渡してから結果を`print`する関数を設定しましょう。 - あとは`d1`などを`ReaderT`のアクションにするだけで、それらをBootTidal.hsから消し去ることができます。 - 残念ながらこの方法を使うと、GHCiに与えた式の結果がすべて当該のモナドのアクションになっていなければならなくなるため、例えば単純な計算結果でさえ`return`をいちいち書かないといけなくなります。しかし、TidalCyclesの利用方法を見る限り、大きな問題にはならないだろうと思います。 -- `ImplicitParams`というGHCの言語拡張を使う - - GHCには、`ImplicitParams`という、もっと直接的にこれを実現する言語拡張があります。文字通り、暗黙の引数を実現するための拡張です([参考](https://qiita.com/philopon/items/e6d2522f5b514c219a5f))。 - これを利用して、例えば`d1`を`?tidal :: Stream => ControlPattern -> IO ()`のように型宣言しておき、`?tidal`(頭に`?`を付けたものが暗黙の引数となります)を暗黙の引数として参照するようにしましょう。後はGHCiの起動時に`?tidal`を定義すれば、`?tidal`の後に`d1`などを定義する必要がなくなるので、BootTidal.hsはもっとコンパクトに済むはずです。 -- その他、`unsafePerformIO`やTemplate Haskellなど、ちょっと危ない手段を使う - - こちらについては詳細を割愛します。`d1`などの再利用性が下がるので、おすすめしません。 - -TidalCyclesの技術的な側面で気になった点は以上です。 -ちょっと難しい話になってしまいましたが、これを機会に、Haskellそのものへの興味を持っていただけると幸いです。 -素晴らしいことに、TidalCyclesそのものはHaskellを知らなくてもそれなりに使えるようになっているようですが、Haskellを知った上で使えば、より簡単にトラブルシューティングができるようになりますし、TidalCyclesをより柔軟に使えるようになるでしょう。 - -もし、今回の記事やTidalCyclesをきっかけにHaskellを勉強してみたいと思ったら、[Haskell-jp Wikiの日本語のリンク集](https://wiki.haskell.jp/Links)を読んで、自分に合う入門コンテンツを見つけてみてください! -それから、何か困ったことがあれば[Haskell-jpのSlack Workspaceにある、#questionsチャンネル](https://haskell-jp.slack.com/messages/C5666B6BB/convo/C4M4TT8JJ-1547294914.091800/)で質問してみてください。 -[登録はこちら](https://haskell.jp/signin-slack.html)からどうぞ! - -それでは2019年もHaskellとTidalCyclesでHappy Hacking!! 🎶🎶🎶 diff --git a/preprocessed-site/posts/2019/unicode-show.md b/preprocessed-site/posts/2019/unicode-show.md deleted file mode 100644 index 88c13b99..00000000 --- a/preprocessed-site/posts/2019/unicode-show.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: 日本語をshowしてうまく表示されなかったら -subHeading: unicode-showの紹介(と、pretty-simpleを少し) -headingBackgroundImage: ../../img/background.png -headingDivClass: post-heading -author: YAMAMOTO Yuji -postedBy: YAMAMOTO Yuji(@igrep) -date: December 22, 2019 -tags: 日本語 -... ---- - -# ℹ️この記事は🎄 - -この記事は、[Haskell Advent Calendar 2019](https://qiita.com/advent-calendar/2019/haskell) 22日目の記事です。 -例年どおりタイプセーフプリキュア!の話をするつもりでしたが、ネタが実装できなかったので[unicode-show](http://hackage.haskell.org/package/unicode-show)の話をします[^precure]。 -まぁ、こちらの方がみなさんにとっては有益でしょうし🙃 - -[^precure]: 例年どおりですとプリキュアAdvent Calendarと同時投稿をしている予定でしたが、例年参加者が減っていたこともあり、今年はプリキュアAdvent Calendarはなくなってしまいました😞 - -# 日本語(などの)話者がHaskellを始めるとあるある - -GHCiに日本語を入力したり... - -```haskell -ghci> "みんなで幸せゲットだよ!" -"\12415\12435\12394\12391\24184\12379\12466\12483\12488\12384\12424\65281" -``` - -日本語を`print`したり... - -```haskell -ghci> print "私、堪忍袋の緒が切れました!" -"\31169\12289\22570\24525\34955\12398\32210\12364\20999\12428\12414\12375\12383\65281" -``` - -日本語を`show`したり... - -```haskell -ghci> iimashita x = "今、" ++ show x ++ "って言いました!?" -ghci> putStrLn (iimashita "ハスケル") -今、"\12495\12473\12465\12523"って言いました!? -``` - -すると、日本語の大半が変な文字列に変わってしまいました😥。 - -へ... 変な文字列じゃないし!エスケープシーケンスに変換しただけだから! - -これは、Haskell標準における`show`関数の残念な仕様です。 -`show`関数に文字列を渡すと、ダブルクォートで囲った上で、ASCII範囲外の文字列や、ASCIIの非表示文字などをエスケープシーケンスに変換して返します。 -これは、`show`関数をデバッグで使用した際、指定した文字列にどんな文字が含まれているか、簡単にわかるようにするための仕様です。 -文字の文字コードを表示すれば、NULL文字や制御文字、ゼロ幅文字、特殊なスペースなど、視認しにくいおかしな文字が含まれていても、一目でわかるのです。 - -しかしこれは日本語話者である我々にとって、少なくとも日本語の文字に関しては「余計なお世話」です。 -NULL文字やASCIIの制御文字といった本来画面に表示することがない文字列ならともかく、ASCII範囲外の文字列すべてをエスケープしてしまうのはやり過ぎでしょう。 -現代はUnicodeがあるおかげで、日本語に限らずともASCII範囲外の文字を扱うのは当たり前になりましたから。 - -# 🌐unicode-showを使おう - -そこで便利なのが[unicode-show](http://hackage.haskell.org/package/unicode-show)です。 -unicode-showの`ushow`関数は、`show`がエスケープシーケンスに変換した日本語などの文字列を、元の文字列に戻してくれます。 -なので、新しい型クラスを定義する必要もなく、そのまま`Show`型クラスのインスタンスを再利用できるのです。 - -早速先ほどの`show`を使った例に適用してみましょう。 - -まずは👇のコマンドでインストールして、GHCiを起動します。 - -```bash -stack build unicode-show -stack exec ghci - -# あるいは、最近のcabalを使っている場合は... -cabal v2-install --lib unicode-show -cabal v2-repl -b unicode-show -``` - -`Text.Show.Unicode`モジュールを`import`して`show`を使っている箇所を`ushow`に変えれば、お望みどおりの挙動になります。 - -```haskell -ghci> import Text.Show.Unicode -ghci> iimashita x = "今、" ++ ushow x ++ "って言いました!?" -ghci> putStrLn (iimashita "ハスケル") -今、"ハスケル"って言いました!? -``` - -わくわくもんですね! - -`print`の例も、`uprint`に変えれば🆗です。 - -```haskell -ghci> uprint "私、堪忍袋の緒が切れました!" -"私、堪忍袋の緒が切れました!" -``` - -ウルトラハッピーですね!! - -さらに、次のコマンドをGHCiに入力すれば、GHCiに直接入力した日本語文字列もそのまま表示されるようになります。 - -```haskell -ghci> :set -interactive-print=uprint -ghci> "みんなで幸せゲットだよ!" -"みんなで幸せゲットだよ!" -``` - -カンペキ✨ - -えっ、常に`uprint`したいからいちいち`:set -interactive-print=uprint`するのが面倒くさい? -そんなあなたは👇を`~/.ghci`に書くことけって~いでしょう。 - -```haskell -import qualified Text.Show.Unicode -:set -interactive-print=Text.Show.Unicode.uprint -``` - -# unicode-showの最近の修正 - -そんなunicode-showですが、残念ながら一昨年、作者である村主崇行さんが亡くなってしまいました[^nushio]。 -日本に住むHaskellerをサポートする日本Haskellユーザーグループとしては、このパッケージをメンテナンスし続けることに大きな意義があると判断し、私はこのパッケージを[Haskell-jp](https://github.com/haskell-jp/)のGitHubリポジトリーでメンテナンスすることにしました。 -以下がそのリポジトリーです。 - -[^nushio]: 村主崇行さんは「[すごいHaskellたのしく学ぼう!](https://shop.ohmsha.co.jp/shopdetail/000000001926/)」の翻訳を担当されるなど、unicode-show以外にも日本のHaskell界に多大な功績をもたらした方でした。 - -技術書典5の「か74」で、矢澤にこ先輩が簡単なHaskellで代数(半群・モノイド・群・環・体)を教えてくれる本を頒布します。
— あいや🤘🙄🤘技術書典5@か74 (@public_ai000ya) 2018年9月29日
よろしくお願いします🐕https://t.co/KBFxqX69m3
☝サークルページ#技術書典 #技術書典5 pic.twitter.com/HvD5ql4gFl