定食屋おろポン

おろしポン酢と青ネギはかけ放題です

Rubyのワンライナーでファイルパスをファイル名に落としこみつつflattenする

自分でもタイトルの意味がわからんけど、要するに

こいつを

./01/hoge.png
./02/fuga.png
./03/01/moga.png
./03/02/piyo.png

こうしたいときがたまにある。

./dest/01-hoge.png
./dest/02-fuga.png
./dest/03-01-moga.png
./dest/03-02-piyo.png

ワンライナーで行ける

% ruby -r FileUtils -e "Dir.glob('**/*.png').each{|path| FileUtils.mv(path, './dest/' + path.gsub(/\//, '-'))}"

怖かったらdry-runすればいい

% ruby -r FileUtils -e "Dir.glob('**/*.png').each{|path| FileUtils.mv(path, './dest/' + path.gsub(/\//, '-'), :noop=>true, :verbose=>true)}"

findとかmvとかsedとか難しいんでこれで。 まじめにやるならFile.joinとか使ったほうが良いとおもう。

なお、この記事は嘘ではない。

楽しく学ぼう公開鍵暗号方式

仕事で「そろそろパスワードやめて公開鍵暗号つかいませんか..?」と近代化をすすめるにあたって、説明資料が必要かと思って公開鍵暗号方式の説明から作り始めた。

よく考えたら、興味のない人には仕組みとかどうでもよくて「なにがよいのか」「どうつかうのか」「なにに気をつけたらいいのか」の3点だけ伝えればよいことに気づいたので、公開鍵暗号方式の説明は業務時間外に書いて公開することにした。

かなり平易に解説したつもりなのでいろいろな人に読んでほしい反面、個人として暗号やセキュリティに造詣が深くない、というかあまり知らないので間違いが含まれていないかが怖いので、斧とか斧とか待ってます。

知識不足なのでRSA暗号自体の仕組み(非対称関数とか)は触れていません。気になったらサイモン・シンの暗号解読あたりを読んでください。

シカクいアタマでハスケルする

中学受験予備校の日能研が昔から、「シカクいアタマをマルくする」という広告を張っている。電車内のドア横・窓上ポスターによく掲載されているため、ご存じの方も多いだろう。

2015年2月掲載分(とウェブサイトに記載されているが、1月時点で既に掲載されている。雑誌の発売月みたいなものだろうか)の問題がこちら。*1

2015年2月掲載 鴎友学園女子中学校【算数】 | シカクいアタマをマルくする。 | 中学受験-小学生のための中学受験塾。日能研

さすがに良問だ。 (2/29を含めて)366日分を全て書き出すと時間が掛かり過ぎるため、「理詰めで短時間で解ける方法があるはずだ」「どうやったら候補を絞り込めるだろうか」と考えさせる。 また、問1と問2でほどよい難易度の上昇がある。 (大人にとっても、電車内で暗算しようとするとかなり歯ごたえがあってよい)

中学受験生にとって良問であると同時に、なにやらプログラミングで解くにも適した問題に見える。

  • 日付ライブラリの基礎的な使用(366日の列挙)
  • 数字のゼロ埋め(1/111ではなく、0101として扱う必要がある)
  • リストの最大、最小、フィルタなどの各種操作

が適度な難易度(中級者以上には、かなり物足りないと言われるかもだけど)で散りばめられている。

Rubyで解いたら作業なので、Haskellで解いてみる。

import Data.Time (Day, fromGregorian, toGregorian)
import Data.List (minimumBy, maximumBy)
import Data.Ord (comparing)

char2segnum :: Char -> Int
char2segnum '0' = 6
char2segnum '1' = 2
char2segnum '2' = 5
char2segnum '3' = 5
char2segnum '4' = 4
char2segnum '5' = 5
char2segnum '6' = 6
char2segnum '7' = 4
char2segnum '8' = 7
char2segnum '9' = 6

countSegnum :: String -> Int
countSegnum = sum . map char2segnum

zeroPadding :: Int -> Int -> String
zeroPadding width = reverse . take width . (++ repeat '0') . reverse . show

allDaysInLeap :: [Day]
allDaysInLeap = [fromGregorian 2000 1 1..fromGregorian 2000 12 31]

gregorian2string :: Day -> String
gregorian2string date = let (_, m, d) = toGregorian date
                        in  zeroPadding 2 m ++ zeroPadding 2 d

day2segnum :: Day -> Int
day2segnum = countSegnum . gregorian2string

dayHasMinSegnum :: (Day, Int)
dayHasMinSegnum = minimumBy (comparing snd) $ zip allDaysInLeap (map day2segnum allDaysInLeap)

dayHasMaxSegnum :: (Day, Int)
dayHasMaxSegnum = maximumBy (comparing snd) $ zip allDaysInLeap (map day2segnum allDaysInLeap)

daysHasSegnum :: Int -> Int
daysHasSegnum n = length $ filter ((==n) . snd) $ zip allDaysInLeap (map day2segnum allDaysInLeap)

main :: IO ()
main = do
  putStrLn $ "1.1: " ++ show dayHasMinSegnum
  putStrLn $ "1.2: " ++ show dayHasMaxSegnum
  putStrLn $ "2: " ++ show (daysHasSegnum 24)

出力はこのとおり

1.1: (2000-11-11,8)
1.2: (2000-08-08,26)
2: 16

日付を扱うのがちょっと大変かと思ったが、実際に試してみると非常に楽で助かった。Day型がEnumインスタンスなので、1/1から12/31の列挙が[fromGregorian 2000 1 1..fromGregorian 2000 12 31]で済むのが素晴らしい。

Ruby(Date.new(2000,1,1)..Date.new(2000,12,31))と全然変わらない。

*1:そもそもウェブサイトに掲載されていることを初めて知った。これからは「シカクいアタマをマルくするが貼ってあるけど人が前に立ってて問題が読めない..死のうかな」といったときもHPを見れば大丈夫だ。

シグルイの最終話でなぜ三重は自害したかの国語的読解

漫画「シグルイ」の最終話でなぜ三重は自害したのか。 「シグルイ 最終回」 「シグルイ ラスト」 などで検索すると、大きく分けて2種類の回答がある。

  • 三重は未だに伊良子を愛していたから
  • 傀儡と化した源之助の姿に絶望を覚えたから

いわゆる受験など国語の問題として考えた時、正解は後者だ。 あくまで国語の問題として扱うため、漫画版「シグルイ」のみを取り扱うし、作者が実際にどう考えていたかではなく「作品から、作者はどう考えていると読み取れるか」に主眼をおく。

結論

  1. 御前試合に臨むまでに三重は源之助と心を交わしていたが、心の奥底では伊良子への想いを断ち切れずにいた
  2. 伊良子との試合中、源之助は虎眼の傀儡だった自分と決別した
  3. 源之助が伊良子に勝利し、三重の伊良子への想いは消滅した
  4. 伊良子の斬首を命じられ、己を殺して源之助は伊良子の首級をとる。封建社会のなかにあって、主君の傀儡であることからは逃れられぬ源之助の姿に三重は絶望を覚え、自害する

それぞれを細かくみていく。

伊良子への想いを断ち切れぬ三重

三重と源之助が心を交わすようになったことは、第八十景からありありと見て取れる。 一方で第八十一景からは、心の奥底では伊良子を愛していたであろうことも読み取れる。 第八十一景の虎口前、伊良子の逆流れで正中線を斬られた藤木の姿をみる。不安のあらわれともとれるが、伊良子の勝利を臨む心が見せた幻覚ともとれる。

斬ってくださいまし 憎い憎い 伊良子を

そもそも源之助に伊良子を斬ってほしいという三重の願いは、伊良子への想いの裏返しでもある。 伊良子への強い憎悪がまだ三重の心に残っていることは、伊良子への愛が未だにくすぶっている証左だ。

斬ってくださいまし 憎い 憎い憎い伊良子を

第一景で描かれる御前試合のイントロでは、肩を震わせながら「憎い」一回増しで伊良子への憎しみを漏らしている。

虎眼の傀儡だった自分との決別

第八十二景の最後で、源之助は刀をかつぐ。 また、第二十一景で不完全な「流れ」をして七丁念仏をあらぬ方向へ飛ばした三重も描かれる。

痩せさらばえた三重の姿と重なるような源之助。担いでいるのはやはり主君から預かった宝刀、七丁念仏だ。これは偶然ではない。

絶対に 落としてはならぬ宝刀であった

第二十一景では、三重の手から離れた七丁念仏が地面に落ちる前に、源之助が見事受け止める。 まず第一に、けして落としてはならない岩元家の大事な刀であるため。また、七丁念仏を三重が地面に落としたとなっては虎眼は三重を許さないからだ。 源之助は、宝刀と三重の両方を守った。

御前試合に戻る。 源之助はかついだ七丁念仏をいくに向かって投げる。 これは、伊良子の下段(逆流れ)を跳ね上がらせるための策だ。太刀の切っ先に反射する太陽光で目をつむるいく。いくの目を通して源之助を見ていた伊良子は、顔を歪ませながら間合いの外から逆流れをはなつ。 伊良子に勝つため、三重と結ばれるために、源之助は岩元家の宝刀を投げ捨てたのだ。

源之助は、ひとたび虎眼に命じられれば操り人形となっていた過去と決別したことを三重に示す。

消滅する伊良子への想い

源之助が勝利した時 三重の深部に潜みし「魔」は 跡形もなく消滅していた

三重に潜んでいた「魔」とは何を指すのか。

これには同じコマで描かれている白い貝殻が重要な意味を持つ。 この貝殻は、特に終盤で頻繁に登場する。

士は貝殻のごときもの 士の家に生まれたる者のなすべきは お家を守る これに尽き申す

この白い貝殻は、さかのぼって第三十六景・第三十七景であらわれる。 虎眼流道場に門下生が大勢いたころの話だ。

第三十六景で、最下層出身の伊良子が虎眼流の同胞に仲間意識を抱き始めたその矢先に、士としての覚悟を伊良子に語る源之助。*1 ここでは、貝殻は「家を守る」士としての覚悟の象徴として描かれる。 話中で直接書かれているとおり、貧農から士へと取り立ててくれた虎眼への恩に報いるためだ。

柔肌に触れることなく男は去った 透けるような白い貝殻を残して

第三十七景で、源之助は三重の寝室に忍び込み、柔肌に触れることなく貝殻を残して立ち去る。 ここでは、貝殻は源之助の真心の象徴だ。貝殻の透き通った白が、三重への真っ直ぐな愛を表している。

第八十三景の最後で描かれた白い貝殻はどちらを示しているのだろうか。 岩元家の宝刀を投げ捨て、虎眼の傀儡として三重を苦しめた過去と決別した矢先のことである。 「家を守る」ではなく、源之助の三重への真っ直ぐな愛を象徴していると見るのが妥当だ。

桜吹雪の中で心と心をつなぎ合った源之助が 乙女の胸の内に潜む何者かの存在を知らぬ筈はない

でも、「何者か」と書かれている。この「何者か」が伊良子であることは自明だ。

であればこそ、三重に潜んでいた「魔」とは、伊良子への憎悪とその裏返しである伊良子への愛を指していると考えられる。 伊良子への愛も憎悪も消え去り、三重の心に残るは源之助の愛のみ。「三重は未だに伊良子を愛していたから」自害した、という答えはこの時点で除外できる。

傀儡

ただ"士"という言葉だけが 体内で反芻していた

伊良子の首級をとるよう命じられた源之助が、葛藤の後に己を殺して断首するまでのシーンに、「傀儡」にまつわる描写が複数でてくる。

  • 虎眼に拾われ、士に取り立ててもらった時の若い源之助と、それを見て微笑む虎眼
  • 浜辺に落ちていた白い貝殻
  • 源之助の鼻血
  • 三重に伊良子の種をつけるため、同じく鼻血を垂らしながら三重の手を押さえつける源之助
  • 血に染まる貝殻

このなかでは特に、三重の手を押さえつける源之助の姿は、そのまま傀儡の象徴となっている。

このシーンが登場する「第九景 傀儡」を少し細かく追ってみる。

幼き頃から嫌というほど見てきた 父の仰せとあらば意志をなくした傀儡となる高弟たち

虎眼流道場では、虎眼の言うことは絶対である。

傀儡… 男はみな傀儡

心を殺すとき、源之助は鼻血を垂らす。源之助は滅多に感情を表に出さないが、裏腹に胸の中には強い意志や情愛を宿している。その源之助が、意志や感情を押さえつけ、命じられたことを命じられたままに行う傀儡にならねばならぬとき、鼻血を垂らすのだ。*2

このときも、源之助は血を床に垂らしながら、三重が動けぬように腕を押さえつけていた。

武家の娘にとって貞操は誇りそのもの 胸の中に輝く真白き打掛

誇りを実の父親と傀儡と化した男たちに踏みにじられようとした三重は、舌をかんで自害しようと思うまでに絶望する。

だからこそ傀儡ではない伊良子に心を寄せたし、ともに生きていく源之助が傀儡であることなど耐えられない。 「傀儡」は三重のトラウマか、それ以上だ。

この象徴的なシーンが断首のシーンに挿し込まれるのは、「傀儡」こそが三重の絶望の引き金となったことを強くほのめかしている。


「源之助の”誇り”そのもの」である伊良子を、晒し首にするため断首するなど、源之助にとっては到底受け入れがたい命令だ。 しかし、世は魔人虎眼ですら社会性を放擲し得ない封建社会である。 「士」というアイデンティティーを捨て去ることができなかった源之助は、己の意志を殺し傀儡となって伊良子の首を落とす。

三重様だけは守り申す いかなる嵐にも屈しませぬ

源之助の誓いは果たされず、絶望した三重は自害した。

*1:タイミングの悪い男だ

*2:第七十七景においても、伊良子とすれ違う際に鼻血を流しているが、仇を目の前にして目を伏せたまま耐えたためだ。

Haskell / トップダウン or ボトムアップ

関数プログラミングの本がでるようです。とりあえずAmazonで予約注文しています。

関数プログラミング実践入門という本を書きました - ぼくのぬまち 出張版

日本語のHaskell本(Haskellを題材にした関数プログラミングを含め)が徐々に増えてきて、にわかHaskellerにとっても大変嬉しいですね。

本題

著者のブログに、RLE(Run Length Encoding)を題材に、トップダウンで書いていく道のりが丁寧に記述された記事があったので、トップダウンの実装について思ったことを書きます。

こちらの記事です。RELの内容もこちらを御覧ください。

孤独のHaskell - ぼくのぬまち 出張版

やっぱりトップダウンですよね

僕も、なるべくHaskellでは「抽象的に、トップダウンに」書いていこうと気をつけています。 試しにRLEを書いてみたところ、似たような道のりをたどって、こんな感じになりました。

rle :: String -> String
rle = concatMap encode . group where
  encode cs = head cs : show (length cs)

でもボトムアップになっちゃうこともあるよね

RLD関数

RLEはトップダウンで書けました。では、その逆の操作は?

つまり、RLEされた「A2B2C3」といった文字列から、「AABBCCC」という元の文字列に復号する関数です。 Run Length Decoding、RLDと名づけます。

トップダウンのアプローチ

これをトップダウンで書いてみます。

「A2B2C3」といった文字列は、「文字、連続回数、文字、連続回数、文字、連続回数..」といった構造になっています。これを、一連の文字列になおすわけですね。

中間構造としては「(文字、連続回数), (文字、連続回数), (文字、連続回数)..」がよいでしょうか。 RLD関数の基本構造は、「文字列」→「(文字、連続回数)のリスト」→「文字列」となります。

コードで示します。

rld :: String -> String
rld = fromIntermediate . toIntermediate where
  fromIntermediate :: [(Char, Int)] -> String
  fromIntermediate = undefined
  toIntermediate :: String -> [(Char, Int)]
  toIntermediate = undefined

さて、fromIntermediateの実装は自明のためよいとして、toIntermediateはどう実装すればよいでしょうか。

  1. Parsecを使う
  2. 正規表現を使う
  3. 地道に実装(文字列の頭から「文字、連続回数」の組を取り出して、残りを再帰で同様に処理する)

選択肢はこのあたりかと思います。この程度の単純なパースであれば、(LLなら迷わず正規表現ですが)3.の地道な実装で十分事足ります。

ここで、とても嫌な予感がします。

ともあれ、実装してみます。

rld :: String -> String
rld = fromIntermediate . toIntermediate where
  fromIntermediate :: [(Char, Int)] -> String
  fromIntermediate = concatMap (\(c,n) -> replicate n c)
  toIntermediate :: String -> [(Char, Int)]
  toIntermediate [] = []
  toIntermediate (c:cs) = let n = takeWhile isNumber cs
                              rest = dropWhile isNumber cs
                          in  (c, read n) : toIntermediate rest

RLDが出来上がりました。やったー!

簡単には簡単にならない

さてと、ひと通り出来上がったところで、このRLD実装を簡単にしてみようと思います。

fromIntermediateにいなくなってもらうのは、非常に簡単です。置換するだけなので。

rld :: String -> String
rld = concatMap (\(c,n) -> replicate n c) . toIntermediate where
  toIntermediate :: String -> [(Char, Int)]
  toIntermediate [] = []
  toIntermediate (c:cs) = let n = takeWhile isNumber cs
                              rest = dropWhile isNumber cs
                          in  (c, read n) : toIntermediate rest

次に、toIntermediateにもいなくなってもらいたいですね。 でも..どうやって?

toIntermediate再帰関数です。この関数には、toIntermediateという名前が付いているため、関数内で再帰呼び出しができます。 toIntermediateをRLDに組み込んでしまうと、関数に名前が付かないのでtoIntermediateの実装を呼び出すことができません。

toIntermediateを消すの簡単には行かず、RLD自体を再帰呼び出し関数に作り替えてやる必要があります。 でも、それって「(文字、連続回数), (文字、連続回数), (文字、連続回数)..」という中間構造を作るという方針から外れるということです。

嫌な予感がした時点でボトムアップ

嫌な予感がした時点でボトムアップに方針転換すると問題は非常に簡単に解けます。

  1. 文字列を受け取ったら、最初の「文字、連続回数」のペアとその後に分ける
  2. 「文字、連続回数」のペアを、文字列に直す
  3. 残りの文字列も同様に処理する

こういう処理の流れで書く、ボトムアップな方針です。

コードで示します。

rld :: String -> String
rld [] = []
rld (c:cs) = let n = read $ takeWhile isNumber cs
                 rest = dropWhile isNumber cs
             in  replicate n c ++ rld rest

あるいは、spanを使って

rld :: String -> String
rld [] = []
rld (c:cs) = let (n,rest) = span isNumber cs
             in  replicate (read n) c ++ rld rest

トップダウンで回り道をするよりも、早く短くシンプルに書けてしまいました。アイヤー。

最終的なコード

参考までにテストコードも含めて置いておきます。

gist1ef0c725af9cf8fcad8a

トップダウン or ボトムアップ

「基本的にトップダウンで、あとはまあ臨機応変に」っていうありきたりな結論が導かれそうなのですが、その臨機応変が難しいですわホンマ。

Rubyのnet/httpで重複するキーをもつパラメータをPostする

net/http を使うと、単純にPostするのは至って簡単だ。

Net::HTTP.post_formメソッドを叩くだけで事足りる。

library net/http

require 'net/http'
require 'uri'

#例1: POSTするだけ
res = Net::HTTP.post_form(URI.parse('http://www.example.com/search'),
                          {'q'=>'ruby', 'max'=>'50'})
puts res.body

#例2: 認証付きで POST する
res = Net::HTTP.post_form(URI.parse('http://jack:pass@www.example.com/todo.cgi'),
                          {'from'=>'2005-01-01', 'to'=>'2005-03-31'})
puts res.body

#例3: より細かく制御する
url = URI.parse('http://www.example.com/todo.cgi')
req = Net::HTTP::Post.new(url.path)
req.basic_auth 'jack', 'pass'
req.set_form_data({'from'=>'2005-01-01', 'to'=>'2005-03-31'}, ';')
res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
case res
when Net::HTTPSuccess, Net::HTTPRedirection
  # OK
else
  res.value
end

キーが重複するパラメータ問題

フォームで配列を送信するとしよう。

例えば、

somekey[] => "hoge",
somekey[] => "fuga",
somekey[] => "piyo",

といったパラメータだ。

post_formメソッドは、uriparamsの引数を取る。第一引数のuriURIで、第二引数のparamsHashだ。 つまり、こういう呼び出しになるだろう。

Net::HTTP.post_form(URI.parse('http://example.com'), {
  "somekey[]" => "hoge",
  "somekey[]" => "fuga",
  "somekey[]" => "piyo",
})

ここで問題が浮上する。Hashは重複するキーを持つことができない。上記の呼び出しは、このように変換されてしまう。

Net::HTTP.post_form(URI.parse('http://example.com'), {
  "somekey[]" => "piyo",
})

解決法

HTTPリクエストbodyにPostしたいデータを直接セットしてやることで回避した。 その際は、自分でPostするパラメータを文字列化し、URLエンコードする必要がある。具体的には、URI.encode_www_formを使う。

3行目を参照。

url = URI.parse('http://www.example.com/todo.cgi')
req = Net::HTTP::Post.new(url.path)
req.body = URI.encode_www_form [
  ["somekey[]", "hoge"], 
  ["somekey[]", "fuga"], 
  ["somekey[]", "piyo"]
]
res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
case res
when Net::HTTPSuccess, Net::HTTPRedirection
  # OK
else
  res.value
end

キモは、URI.encode_www_formでURLエンコードする際に、配列の配列を渡すことだ。Hashじゃないからキーの重複も問題ない。

http://docs.ruby-lang.org/ja/2.1.0/class/URI.html#S_ENCODE_WWW_FORM

URI.encode_www_formメソッドの返り値はこうなる。

"somekey%5B%5D=hoge&somekey%5B%5D=fuga&somekey%5B%5D=piyo"

tips

Net::HTTP.post_formはHashを受け取ると書いたが、それはあくまでドキュメント上のはなしだ。 ソースを追えばわかるが、内部ではURI.encode_www_formを呼んでいるだけである。 (post_formの中でNet::HTTPHeader#set_form_dataが呼ばれ、その中でURI.encode_www_formしている。)

だから、post_formメソッドに(Hashではなく)配列の配列を渡しても動く。

Net::HTTP.post_form(URI.parse('http://example.com'), [
  ["somekey[]", "hoge"], 
  ["somekey[]", "fuga"], 
  ["somekey[]", "piyo"]
])

不安なら、set_debug_output($stderr)を使ってリクエストを覗いてみればいい。

ただundocumentedなので、こういう風に使っていいのかアヤシイ。

中間値のroundによる丸め 〜Haskellのround関数を見て吹き出した件〜

C Way

round(1.5)は、2になります。 round(2.5)は、3になります。

それが、C言語流。俗にいう「四捨五入」の処理です。

ISO/IEC 9899 p233にも記載されています。

The round functions round their argument to the nearest integer value in floating-point format, rounding halfway cases away from zero, regardless of the current rounding direction.

すなわち

round関数は、引数のもっとも近い整数へと丸めます。 2つの整数の中間の値だった場合はゼロから遠ざかるように丸めます。

roundイコール四捨五入。そう、世の中はCで出来ている。

Haskell Way

Haskellでのroundはどうでしょうか。

Haskellのドキュメントを見て驚きました。

round x returns the nearest integer to x; the even integer if x is equidistant between two integers

すなわち

round xは、xにもっとも近い整数値を返します。2つの整数の中間の値だった場合は偶数を返します。

「偶数を返します。」

_人人人人人人人人人_
> 偶数を返します <
 ̄Y^Y^Y^Y^Y^Y^Y^Y ̄

round 2.5は2になり、round 3.5は4になります。

何故そうした!!!!

僕が無知でした

すこし調べた結果、単に僕が無知だっただけだということがわかりました。

roundには「rounding mode」というモードがあり、言語によってはモードを指定できます。 もっとも近い整数値を返すrounding modeの中で代表的なのは下記の4種類のようです。

  • HALF_UP
    • 中間値: Away from zero
  • HALF_DOWN
    • 中間値: Toward zero
  • HALF_EVEN
    • 中間値: Even integer
  • HALF_ODD
    • 中間値: Odd integer

CやRuby等ではHALF_UPを、HaskellではHALF_EVENを採用しているに過ぎないということですね。 また、PythonもHALF_EVENです。*1

「中間値では偶数に丸める」と書くと少し奇妙に思えますが、「中間値ではIntの最下位ビットを0にするポリシーである」と考えればそこまで奇妙でもありません。

負値の丸め

マイナスの値に関しても、中間値をroundしたときの挙動は言語によって違います。

C

Cでは負値であっても「ゼロから遠ざかるように」丸めます。1.5を丸めると2になりますし、-1.5を丸めると-2になります。

Java

一方、Javaでは「1/2を加えて切り捨て」のために正値ではゼロから遠ざかりますが、負値ではゼロに近づきます。1.5を丸めると2ですが、-1.5を丸めると-1になります。

負値に対するceilfloorが言語によって挙動が違うのは有名な話ですが、中間値のroundも挙動が違うわけですね。

そもそも

1.5にせよ-2.5にせよ、有理数ライブラリを使うのでもなければ浮動小数点数なので近似値でしかない。 中間値をroundしたときの挙動が問題になる場合はコードに問題があることがほとんどでは、という感想もある。

とはいえ

Haskellでも四捨五入を行ないたい!!!

Preludeでのroundの実装を少し変えて、C言語スタイルのroundを書きました。

roundUpOn5 :: (RealFrac a, Integral b) => a -> b
roundUpOn5 x =  let (n,r) = properFraction x
                    m     = if r < 0 then n - 1 else n + 1
                in case signum (abs r - 0.5) of
                  -1 -> n
                  0  -> m
                  1  -> m
                  _  -> error "round default defn: Bad value"
*Main> roundUpOn5 0
0
*Main> roundUpOn5 (2.7)
3
*Main> roundUpOn5 (-2.7)
-3
*Main> roundUpOn5 (-1.5)
-2
*Main> roundUpOn5 (-2.5)
-3
*Main> roundUpOn5 (2.5)
3

追記:より簡潔な実装をコメントでご指摘いただきました。ありがとうございます!

roundUpOn5 :: (RealFrac a, Integral b) => a -> b
roundUpOn5 x
  | n <= -0.5 = m - 1
  | n >= 0.5 = m + 1
  | otherwise = m
  where (m, n) = properFraction x