LoginSignup
2
1

More than 5 years have passed since last update.

「Haskell入門ハンズオン #2」の当日用の資料(2)

Last updated at Posted at 2017-11-17

「Hskell入門ハンズオン #2」の当日用の資料(2)

関数適用演算子と関数合成演算子

関数適用演算子

関数適用演算子(\$)を紹介する。ここで、逆数をもとめる関数recipについて考える。

> recip 8
0.125

これは、「関数recipを数値8に適用した」ということだ。演算子(\$)は、第1引数の関数を第2引数の値に適用する。

> recip $ 8
0.125

「なぜ、こんな演算子が定義されているのだろう?」「必要ないのでは?」関数適用演算子(\$)のレーゾンデートル(存在意義)は?つぎの例をみてみよう。

> logBase 2 (recip (3 + 5))
-3.0

3 + 5の逆数が2の何等であるかをもとめている。このくらいならいいが、「逆数の対数の絶対値の...」のように、長々と空いていくことを考えよう。

abs (logBase 2 (recip (...)))

最後に閉じ括弧が...))))))))のように続くことになる。つぎの例をみてみよう。

> logBase 2 $ recip $ 3 + 5
-3.0

関数適用演算子(\$)なら...))))))))のようにはならない。関数の適用の連鎖が長々と続いたりする。

abs $ logBase 2 $ recip $ ...

丸括弧が入れ子になるよりスマートに書ける。

関数合成演算子

関数合成演算子を紹介する。小文字にして文字コードをもとめる関数の例でみていこう。

> :module Data.Char
> fun c = ord (toLower c)
> fun 'Y'
121

この関数funは関数ordとtoLowerとを、くっつけたものだ。このように「くっつける」ことを「関数合成」と呼ぶ。関数合成演算子(.)を使った関数fun2は、つぎのようになる。

> fun2 = ord . toLower
> fun2 'Y'
121

関数funよりもfun2のほうが、「小文字化して、文字コードをもとめる」という意味を、直接的に表現できている。

関数適用演算子を関数合成演算子に置き換える

つぎのように関数適用を2回以上使う例をみる。

> abs $ logBase 2 $ recip 8
3.0

これを、つぎのように書き換えることができる。

> abs . logBase 2 $ recip 8
3.0

結合力が演算子(.)のほうが(\$)よりも強いことに注意する。また、演算子(\$)は右結合だ。よって、それぞれについて、丸括弧を明示すると、つぎのようになる。

abs $ (logBase 2 $ recip 8)
(abs . logBase 2) $ recip 8

「8の逆数の対数をもとめて、絶対値をもとめる」のと、「8の逆数に、絶対値関数と対数関数を合成したものを適用する」のふたつは、おなじ意味になる。

演算子(\$)が連続したとき、1番右の(\$)以外は、すべて、演算子(.)に置き換えられる。逆に、演算子(\$)が一番右にあるとき、その左の演算子(.)の連続は、すべて、演算子(\$)に置き換えて、解釈できる。理屈を理解しなくても、つぎのような置き換えができることだけ、覚えておこう。

f $ g $ h $ i $ j $ ... $ z x
f . g . h . i . j . ... $ z x

個人的には前者のような、かたちがあらわれたら、後者のように書き換えることにしている。「見ため」がきれいだからだ。

いくつかの関数を学ぶ(1)

Haskellで、はじめから定義されている関数のうちのいくつかを紹介する。ここでは、つぎの7個を学ぶ。

  • 関数map, filter, take, drop
  • 関数replicate, reverse
  • 演算子(++)

関数map

関数mapは、リストのすべてについて、おなじ変換をする。「変換」をあらわす関数を第1引数にする。

> double x = x * 2
> map double [1, 2, 3, 4, 5]
[2,4,6,8,10]

第1引数に「引数を2倍にする関数」をとり、リストのすべての要素に、それを適用している。

関数リテラル

ところで、関数mapの第1引数とするために、つぎのような関数doubleを定義した。

double x = x * 2

この関数doubleは、これ以降、使う予定はない。それなのに、いちいち名前をつけるのは、めんどうだ。このようなとき関数リテラル(無名関数)を使う。

> map (\x -> x * 2) [1, 2, 3, 4, 5]
[2,4,6,8,10]

関数リテラルの記法は、つぎのようになる。

\[仮引数1] [仮引数2] ... -> [式]

関数filter

リストから条件を満たす値だけ取り出す関数だ。リストから奇数だけを取り出してみる。

> filter odd [1, 2, 3, 4, 5]
[1,3,5]

第1引数である関数oddによって、奇数かどうかを判定する。

関数replicate

おなじ値をくりかえしたリストを作成する。

> replicate 10 123
[123,123,123,123,123,123,123,123,123,123]

整数値123を10回くりかえしたリストだ。

関数take, drop

リストの前のほうのいくつかを取り出す。

> take 3 [1, 2, 3, 4, 5]
[1,2,3]

リストの前のほうのいくつかを落とす。

> drop 3 [1, 2, 3, 4, 5]
[4,5]

それぞれ、前のほうの3つの要素、前のほうの3つ以外の要素が、取り出されている。

関数reverse

リストを逆順にする関数だ。

> reverse [1, 2, 3, 4, 5]
[5,4,3,2,1]

リストが逆順になった。

演算子(++)

ふたつのリストを結合する。

> [1, 2, 3] ++ [4, 5]
[1,2,3,4,5]

リストが結合された。Haskellでのデフォルトの文字列は文字のリストなので、文字列は演算子(++)で結合できる。

> "Hello" ++ "World"
"HelloWorld"

まとめ

ここまで紹介してきた関数(mapから演算子(++)まで)は、どれも自分で定義できる。定義は再帰的になる。時間の関係で、ここでは説明しない。これらの関数を使いこなせるようになろう。ここで学んだ関数は、つぎの7個である。

  • 関数map, filter, replicate
  • 関数take, drop
  • 関数reverse, 演算子(++)

いくつかの関数を学ぶ(2)

さらに、つぎの4個の関数を学ぶ。

  • 関数randoms, mkStdGen
  • 関数unlines
  • 関数read

関数randoms, mkStdGen

乱数のリストを生成する関数を紹介する。関数randomsなどは、現在では、標準的なパッケージには含まれていないので、いちど対話環境をぬけて、randomsパッケージを導入しながら対話環境を立ち上げる。

> :quit
% stack ghci --package random

モジュールSystem.Randomを導入する。

> :module System.Random

関数randomsは「乱数の種」を引数として、ランダムな値を要素とする無限リストをかえす。「乱数の種」は関数mkStdGenで整数から作れる。

> take 10 . randoms $ mkStdGen 8
[-398575370259562870,-6370604356117182359,
8399777519602674086,...

関数unlines

文字列を要素とするリストを、行の集わりと解釈して、それぞれの行のおわりに改行を追加した、ひとつの文字列としてかえす関数。

> unlines ["hello", "world"]
"hello\nworld\n"

helloやworldの、それぞれのうしろに\n(改行)が追加される。そのうえで、結合され、ひとつの文字列になる。

関数read

文字列を適切な値に変換する。

> read "12345" + 54321
66666

文字列"12345"が整数値12345に変換された。

入出力を学ぶ

関数

いろいろな関数をみてきた。関数に引数をあたえたものが値に評価される。たとえば、つぎのような計算を考える。

> 3 + 2 * 5
13

関数(演算子)である(+)や(*)に対して、整数値3, 2, 5をあたえることで、式3 + 2 * 5が作られ、それが値13に評価される。関数は引数をとり、それによって作られた式が評価される。関数にできることは、それだけだ。

動作

式を値に評価すること。たしかに、それができるのは、すばらしいこと。しかし、僕らがプログラミングでやりたいことは、それだけじゃない。ウェブサーバを作るにも、ゲームを作るにも、何らかの「動作」が必要になる。「純粋な関数型」のわくぐみで「動作」をあつかいたい。どう考えるか?基本になる「動作」を、関数によって組み合わせればいい。関数putStrLnは、文字列を表示する。

> putStrLn "hello"
hello
> putStrLn "world"
world

putStrLn "hello"は「helloと表示する動作」に評価される。対話環境は、評価の結果が「動作」になったとき、その動作を「実行」する。これらの動作を、「XのつぎにYを実行する」のように組み合わせる。

> putStrLn "hello" >> putStrLn "world"
hello
world

「基本になる動作」を関数によって組み合わせて、「複雑な動作」を組み立てていく。ファイルaction.hsに、つぎのように定義する。

action.hs
hello = putStrLn "hello" >> putStrLn "world"

対話環境に読み込み、試してみる。

> :load action.hs
> hello
hello
world

動作を「関数」で組み合わせていくことで、複雑な動作を組み立てていくことができる。演算子(>>)で動作を組み合わせて、より複雑な動作を組み立てていく。もちろん、それは「いい考え」だ。しかし、たとえばレシピを考えてみよう。

鍋を火にかける
沸騰したらパスタを入れる
パスタが、やわらかくなったらザルにあげる

このように、順番に書かれた内容は、順番に実行されることが期待される。Haskellでは、明示的に演算子(>>)を書かなくてすむ「do記法」という書きかたが用意されている。do記法でaction.hsの関数helloを書き換えてみよう。

action.hs
hello = do
        putStrLn "hello"
        putStrLn "world"

このように明示的な演算子(>>)を書くかわりに、動作を並べて書くことができる。注意する点は、列挙される動作は、予約語doのある行よりも深くインデントし、それぞれの「列挙される動作」どうしのインデントは「そろえる」ということだ。どうだろうか。「ふつうの言語」の書きかたに近くなったのではないだろうか。

入力

関数putStrLnによって作られる動作は「出力」だ。動作には「出力」だけでなく「入力」もある。たとえば、ユーザの打ち込んだ文字列を取り込みたい。そんなときは、「値をかえす動作」を使う。

> getLine
(helloと入力する)hello
"hello"

動作getLineはユーザからの入力を待ち、打ち込まれた文字列を動作からかえる値とする。対話環境は式が「値をかえす動作」に評価されたとき、その動作を実行し、さらに、かえされた値を表示する。かえされた値を使うことを考えよう。名前を入力すると、「Hello, 誰々!」のように、あいさつしてくれる動作をつくる。ファイルaction.hsに、つぎの関数を定義する。

action.hs
greeting = do
        name <- getLine
        putStrLn $ "Hello, " ++ name ++ "!"

試してみる。

> :reload
> greeting
(自分の名前を入力)Yoshikuni
Hello, Yoshikuni!

つぎのように「<-」を使うことで、動作からかえされた値を変数に束縛できる。

var <- action

この変数varは、このあとに列挙される動作のなかで、使うことができる。

打ち込まれた文字列を数値に変換し、それを摂氏温度として、絶対温度に変換された値をかえす動作を考える。ファイルaction.hsに、つぎの関数を定義する。

action.hs
getAbsolute = do
        c <- getLine
        return $ read c + 273

試してみる。

> :reload
> getAbsolute
(25と入力)25
298

動作getAbsoluteの最後の行では、returnという関数が使われている。関数returnは「何もせずに」引数の値をかえす動作を作る。この動作を最後に置くことで、動作getAbsoluteで、絶対温度の値をかえすことができる。

入出力に関する関数、動作

いくつかの入出力に関する関数、動作を学ぶ。ここで学ぶ関数、動作は、つぎの6個だ。

  • 関数putStr, print, 動作getChar
  • 動作getArgs
  • 関数hGetBuffering, hSetBuffering

関数putStr

すでに関数putStrLnをみた。

> putStrLn "hello"
hello

関数putStrLnは引数である文字列を、標準出力に出力したあと、改行を出力する。関数putStrは改行を出力しない。

> putStr "hello"
hello>

改行が出力されないので、プロンプトが出力のあとに続く。

関数print

関数printは文字列ではなく「値」を表示する。

> print 123
123

値を文字列化する関数showがある。

> show 123
"123"

関数printは関数putStrLnとshowを結合したものだ。

> (putStrLn . show) 123
123

実は、対話環境は、評価した結果が動作でないとき、結果の値を関数printに引数としてあたえ、作られた「動作」を実行していた。よって、対話環境では、つぎのふたつの結果がおなじになる。

123
print 123

関数getChar

ユーザの打ち込んだ文字列を行単位で取得する関数getLineはすでにみた。ユーザの打ち込んだ文字を、ひとつだけ取得する関数getCharをみてみよう。

> getChar
(cと入力する)c'c'

動作getCharは入力された文字をかえす。かえされた文字は対話環境によって表示される。

動作getArgs

ターミナルから呼び出すアプリケーションを作っていると、コマンダライン引数を取得したくなる。動作getArgsを使う。ファイルaction.hsに、定義を追加する。

action.hs
printArgs = do
        as <- getArgs
        print as

関数getArgsは、モジュールSystem.Environmentから公開されている。ファイルの先頭に、つぎのように追加する。

action.hs
import System.Environment

モジュールを導入するには予約語importを使う。対話環境で試してみる。

> printArgs
[]

対話環境なので、コマンドライン引数は指定されていない。動作getArgsのかえすリストは空リストになる。対話環境のなかでコマンドライン引数を指定するには、コマンド:runを使う。

> :run printArgs hello world
["hello","world"]

バッファリング

バッファリングというものがある。バッファリングのモードを変えることで、一文字ずつ入力を行うのか、一行ずつ入力を行うのか、を変えることができる。現在のバッファリングのモードを確認する。

> :module System.IO
> hGetBuffering stdin
NoBuffering

現在のバッファリングのモードはNoBufferingである。つまり、入力は一文字ずつ処理される。

> getChar
(cと入力)c'c'

バッファリングのモードを変えるには、関数hSetBufferingを使う。

> hSetBuffering stdin LineBuffering
> hGetBuffering stdin
LineBuffering
> getChar
(c123と入力し改行を入力)c'c'
> 123
123

改行を入力するまで、文字'c'は読み込まれない。1文字読み込んだあとの文字列"123"は、対話環境への「つぎの入力」とされる。

モジュールと実行可能ファイル

プログラムを組みうえで、モジュール分割という考えかたは重要だ。Haskellでは1モジュール、1ファイルとなる。モジュールの作りかたをみる。また、単独で実行可能なファイルの作りかたをみる。

モジュール

適当なモジュールを作ってみる。つぎのようなファイルSome.hsを作る。

Some.hs
module Some where

fun x = x * x

対話環境に読み込む。

> :load Some.hs
*Some> fun 8
64

自分で作ったモジュールを、定義ファイルのなかで、使うことができる。ファイルother.hsを作成する。

other.hs
import Some

f = fun . fun

対話環境で試してみよう。

> :load other.hs
> f 3
81

プロンプトに、そのモジュールの名前が表示される。

実行可能ファイル

実行可能ファイルを作るには、動作で「特別な変数main」を束縛する。つぎのようなファイルhello.hsを作る。

hello.hs
main = putStrLn "Hello, world!"

つぎのようなコマンドを実行する。

% stack ghc -- hello.hs -o hello

すると、実行可能ファイルhelloが作成される。

% ./hello
Hello, world!

資料(3)へ続く

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1