なんだこれは

はてなダイアリーから移転しました。

これってどう使うの?(マイナス・ゼロ・テン・ゼロ)

曖昧さ回避

この記事は 関西Lispユーザ会アドベントカレンダーの6日目の記事です。

広瀬正の小説については Wikipediaの記事などを参照してください。

概要

『マイナス・ゼロ・テン・ゼロ』は、現在の多くのプログラミング言語が採用している、IEEE754によって規定された数です。Common Lispの実装でもさまざまな箇所で影響を受けています。

本記事は、-0.0を題材にいろいろとCommon Lispで遊ぶ記事になります。

-0.0 ;-> -0.0

なお、Common Lispでは『マイナス・ゼロ』を評価すると正のゼロになります。

-0 ;-> 0

Clozure CL 1.11編

-0.0という数字が表現できると、初等数学関数で便利らしい[要出典]のでIEEEで定義でされている。IEEEを参考にしている、C言語でも、もちろん -0.0が使える。

C言語の例

// gcc this.c -o a.out && ./a.out
#include <stdio.h>
#include <stdlib.h>

int main(void){
	float pzero = 0.0;
	float nzero = -0.0;

	printf("%f\n", pzero);  // -> 0.000000
	printf("%f\n", nzero);  // -> -0.000000

	return 0;
}
-0.0の評価

Common Lispでも -0.0が使える、はずです。逸般の家庭にある[要出典]、Clozure CL 1.11のREPLで試してみよう。

-0.0 ;-> -0.0
(and (numberp -0.0)
     (not (intergerp -0.0))
     (zerop -0.0)) ;-> T

これで、-0.0が評価できました。-0.0 は -0.0 と評価されます。また、-0.0は数であり、整数ではないが0であると評価されました。(Common LispではTは真、NILが偽を表します。)

-0.0 と 0.0 の比較

次は 0.0 と比較してみよう。Common Lispの比較といえば、eq, eql, equal, equalp そして数学比較述語の=です。

(format t "eq 0.0 -0.0 ~a~%"  (eq 0.0 -0.0))         ; -> NIL
(format t "eql 0.0 -0.0 ~a~%"  (eql 0.0 -0.0))       ; -> NIL
(format t "equal 0.0 -0.0 ~a~%"  (equal 0.0 -0.0))   ; -> NIL
(format t "equalp 0.0 -0.0 ~a~%"  (equalp 0.0 -0.0)) ; -> T
(format t "= 0.0 -0.0 ~a~%"  (= 0.0 -0.0))           ; -> T

なるほど。値を比較するequalpと数学的な比較を行う=は真を返す。これは納得のいく結果です。

DRY

しかしここまででかなり重複したコードを書くハメになっている。DRYの原則に反している。Common Lispなら補助関数とマクロの出番だ。

(defun report-result (form result)
  (format t "~a ;-> ~a~%" form result))

(defmacro check (form)

  `(report-result ',form ,form))

(check (eq 0.0 -0.0))     ; (EQ 0.0 -0.0) ;-> NIL
(check (eql 0.0 -0.0))    ; (EQL 0.0 -0.0) ;-> NIL
(check (equal 0.0 -0.0))  ; (EQUAL 0.0 -0.0) ;-> NIL
(check (equalp 0.0 -0.0)) ; (EQUALP 0.0 -0.0) ;-> T
(check (= 0.0 -0.0))      ; (= 0.0 -0.0) ;-> T

先程と同じ結果ですね。問題ないようです。
C言語ではどうするんでしょうか?がんばってください。

0.0と-0.0の和差積演算


次に、0.0と-0.0を単純な関数に適用してみましょう。

(check (+ 0.0 0.0))       ; (+ 0.0 0.0) ;-> 0.0
(check (- 0.0 0.0))       ; (- 0.0 0.0) ;-> 0.0
(check (* 0.0 0.0))       ; (* 0.0 0.0) ;-> 0.0
(check (+ -0.0 0.0))      ; (+ -0.0 0.0) ;-> 0.0
(check (- -0.0 0.0))      ; (- -0.0 0.0) ;-> -0.0
(check (* -0.0 0.0))      ; (* -0.0 0.0) ;-> -0.0
(check (+ 0.0 -0.0))      ; (+ 0.0 -0.0) ;-> 0.0
(check (- 0.0 -0.0))      ; (- 0.0 -0.0) ;-> 0.0
(check (* 0.0 -0.0))      ; (* 0.0 -0.0) ;-> -0.0
(check (+ -0.0 -0.0))     ; (+ -0.0 -0.0) ;-> -0.0
(check (- -0.0 -0.0))     ; (- -0.0 -0.0) ;-> 0.0
(check (* -0.0 -0.0))     ; (* -0.0 -0.0) ;-> 0.0
平方根

0.0と-0.0の平方根をとると、奇妙な結果となります。

(check (sqrt 0.0))   ; (SQRT 0.0) ;-> 0.0
(check (sqrt -0.0))  ; (SQRT -0.0) ;-> -0.0

もちろん、(* -0.0 -0.0) は 0.0を返します。

しかし、負零の平方根IEEE754で定義されており、これにそって定義されていると解釈できます。

参考:hyperspecはこちらです。

実装依存の確認

実装依存とは

Common Lispは実装がいくつかあるため、これらの振舞いが実装依存である可能性があります。規格で未定義ということです。

Cで例えるなら、コンパイラバージョンによる振舞いの違い、コンパイラ依存の拡張、コンパイラのバグ、規格のバージョンによる違いに相当します。

実装依存かどうかを確認するには別の実装で動作を確認することが一番でしょう。

Common Lispの主な実装にはつぎのものがあります。

  • Clozure CL

Mac App Storeからインストールできる唯一のCL。マカーの80%はインストールしている[要出典]。

  • Steel Bank CL

フリーのCLの中で最も高速で動作するバイナリが作成できる。ユーザー数も一番多い[要出典]。

  • Embedded CL

CLを、2016年現在使うべきでないとされる、C言語に変換するCL。強い。[あいまいな表記]。

roswellの紹介と処理系のインストール

Common Lispで実装をきりかえて動作させるなら、roswell でros scriptを書くのが一番です。roswellを使いましょう。

roswell の導入については優れた記事、Lisp Advent Calendar 2017の二日目のいまから始めるCommon Lispがありますのでこちらを参考にするとよいでしょう。

roswell をセットアップ(ros setup)すると、sbclのバイナリがインストールされています。

次のコマンドで、roswellがインストールした処理系の一覧を表示しましょう。

ros list installed

次のコマンドで、roswellがインストールできる処理系を表示できます。

ros list versions

また、次のコマンドのように処理系を指定して(ここではClozure CL)、インストールのできるバージョン(ここでは利用可能なClozure CLのバージョン)が表示できます。

ros list versions ccl-bin


適当にインストールしましょう。次のものは例です。

ros install sbcl         #sbclの最新をソースコードからインストール
ros install ccl-bin/1.11 #clozure clの1.11 をインストール
ros install ecl          #eclの最新をソースコードからインストール

roswellは clozure clを公式サイトからダウンロードするため、githubの最新ではありません。(2017年12月10現在)
なお、ソースコードからのインストールはコンパイルに意外と時間がかかるかもしれません。
インストールが終了したあと、つぎのコマンドでインストールができたかどうか確認しましょう。

ros list installed

なお、roswellで処理系を変更するには、ros list installedで表示される処理系/versionを、次のように指定します。

ros use sbcl/1.4.2        #sbcl/1.4.2をデフォルトにする。
ros script化

Common Lispを実行しやすくするために、ros scriptを作成しましょう。ros script化でさまざまな処理系のcommon lispで簡単に実行できるようになります。次のコマンドでひながたをつくって、それを編集します。

ros init minus-zeros
emacs minus-zeros.ros

さて、さきほどのものをそのまま詰め込んでみます。

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  ;;#+quicklisp (ql:quickload '() :silent t)
  )

(defpackage :ros.script.minus-zero.3721803776
  (:use :cl))
(in-package :ros.script.minus-zero.3721803776)


(defun report-result (form result)
  (format t "~a ;-> ~a~%" form result ))

(defmacro check (form)
  `(report-result ',form ,form))

(defun main (&rest argv)
  (declare (ignorable argv))

  (format t "compare 0.0 with -0.0~%")

  (check (eq 0.0 -0.0))
  (check (eql 0.0 -0.0))
  (check (equal 0.0 -0.0))
  (check (equalp 0.0 -0.0))
  (check (= 0.0 -0.0))
  
  (format t "simple operation 0.0 and -0.0~%")

  (check (+ 0.0 0.0))
  (check (- 0.0 0.0))
  (check (* 0.0 0.0))
  (check (+ -0.0 0.0))
  (check (- -0.0 0.0))
  (check (* -0.0 0.0))
  (check (+ 0.0 -0.0))
  (check (- 0.0 -0.0))
  (check (* 0.0 -0.0))
  (check (+ -0.0 -0.0))
  (check (- -0.0 -0.0))
  (check (* -0.0 -0.0))

  (format t "square root with 0.0 -0.0 ~%")

  (check (sqrt 0.0))
  (check (sqrt -0.0))
  )

;;; vim: set ft=lisp lisp:

ros scriptは実行権限が自動的に付与されているのでそのまま実行できます。

./minus-zeros.ros

無事実行できましたね。

処理系依存かどうか比較する

では適当にインストールした処理系でそれぞれを実行してみましょう。

ros use sbcl/1.4.2
./minus-zeros.ros | tee sbcl142.txt
ros use ccl-bin/1.11
./minus-zeros.ros | tee cclb111.txt
ros use ecl/16.1.3
./minus-zeros.ros | tee ecl1613.txt

でその結果(の差異)がこちらです。

;;;; sbcl/1.4.2 debian 9

(check (+ -0.0 -0.0)) ; -> -0.0

;;;; ccl-bin/1.11

(check (+ -0.0 -0.0)) ; -> -0.0

;;;; ecl/16.1.3

(check (+ -0.0 -0.0)) ; -> 0.0

なぜか、eclはsbclとcclと違うようですね。処理系依存でしょうかそれとも規格違反でしょうか?

感想

  • -0.0はCommon Lispで使えるようだが何に使うのかは不明
  • -0.0の演算には処理系依存かなにかがあるかもしれないが、実害があるかあやしい。
  • DRY は大事
  • roswell で処理系を簡単にいれかえられる