getArgs, maxBound, float division: pure functions?
The question of getArgs as a pure function has already been debated on this list back in January: http://www.haskell.org/pipermail/haskell/2005-January/015184.html The topic seems worth revisiting as it expands to a bigger, and perhaps, a bleaker picture. To recap, there were two arguments for getArgs to remain with the IO [String] type: 1. getArgs is the function of the environment and does not have a clear denotation. The value of getArgs may change from one program run to another. 2. The existence of System.Environment.withArgs Regarding argument 1: the value of |maxBound :: Int| is also the function of the environment. Haskell98 Report says [p82, Section 6.4] The finite-precision integer type Int covers at least the range [ - 2^29 , 2^29 - 1 ]. As Int is an instance of the Bounded class, maxBound and minBound can be used to determine the exact Int range defined by an implementation. Thus, the value of |maxBound :: Int| may conceivably change from on program run (under runhugs32 on an AMD64 platform) to another (under runghc64 on the same platform). The Haskell98 Report, in section 6.4.6 defines other implementation-dependent functions: floatRadix, floatDigits, and floatRange. The GHC documentation [ghc6/libraries/base/Prelude.html#t%3ARealFloat] is particularly revealing: floatRadix :: a -> Integer a constant function, returning the radix of the representation (often 2) Some may find the phrase ``constant function that often [sic!] has the value 2'' to be incompatible with the notion of a pure function. Regarding argument 2: the existence of System.Environment.withArgs seems to doom getArgs to remain with the IO type. It cannot be a pure function. The simple extension of the argument points however to inconsistency with the floating-point facility. Haskell98 Report permits an implementation to use IEEE FP for Haskell Floats and Doubles. The Report specifically provides the class RealFrac to give a program access to some aspects of the IEEE FP system. IEEE FP computations are sensitive to the rounding mode, which is observable in pure code. The rounding mode can be changed. The following simple program shows
{-# OPTIONS -fglasgow-exts #-} -- Tested on GHC 6.4 on on i686/Linux
module FP where import Foreign.C
eps ::Float = 2^^(1-24)
-- from /usr/include/bits/fenv.h
type FP_RND_T = CInt -- fenv.h
eFE_TONEAREST = 0 eFE_DOWNWARD = 0x400 eFE_UPWARD = 0x800 eFE_TOWARDZERO = 0xc00
foreign import ccall "fenv.h fegetround" fegetround :: IO FP_RND_T
foreign import ccall "fenv.h fesetround" fesetround :: FP_RND_T -> IO FP_RND_T
test1 = do let tf () = 1 + eps/2 let tfe = tf () putStrLn "Rounding mode and the result" fegetround >>= print print $ tf () print $ tfe
old <- fesetround eFE_UPWARD putStrLn "Rounding mode and the result" fegetround >>= print print $ tf () print $ tfe
fesetround old putStrLn "Rounding mode and the result" fegetround >>= print print $ tf () print $ tfe print "Done"
that the supposedly pure computation "1 + eps/2" is not, in fact, referentially transparent. Should all floating-point computations be put in the IO monad? Granted, FP addition, among other operations, is not associative; so one may even wonder if FP can be done in any monad at all. OTH, when it comes to IO, the monad laws are usually stretched.
oleg@pobox.com writes:
Regarding argument 1: the value of |maxBound :: Int| is also the function of the environment. Haskell98 Report says [p82, Section 6.4]
The finite-precision integer type Int covers at least the range [ - 2^29 , 2^29 - 1 ]. As Int is an instance of the Bounded class, maxBound and minBound can be used to determine the exact Int range defined by an implementation.
Thus, the value of |maxBound :: Int| may conceivably change from on program run (under runhugs32 on an AMD64 platform) to another (under runghc64 on the same platform).
I guess we could declare 32- and 64-bit Ints to be distinct types, and then do a system call to get the native Int type. data SomeIntegral = forall a. Integral a => SomeIntegral a nativeInt :: IO SomeIntegral main = case nativeInt of SomeIntegral (_::int) -> print (maxBound :: int) The disadvantage would be the need for polymorphism over the Integral class whenever we wanted native-sized integers.
Regarding argument 2: the existence of System.Environment.withArgs seems to doom getArgs to remain with the IO type. It cannot be a pure function. The simple extension of the argument points however to inconsistency with the floating-point facility. Haskell98 Report permits an implementation to use IEEE FP for Haskell Floats and Doubles. The Report specifically provides the class RealFrac to give a program access to some aspects of the IEEE FP system. IEEE FP computations are sensitive to the rounding mode, which is observable in pure code. The rounding mode can be changed. [...] Should all floating-point computations be put in the IO monad?
In principle, you could use seperate types to distinguish floats with different rounding modes, but I imagine this would be difficult or annoying to implement. -- David Menendez <zednenem@psualum.com> <http://www.eyrie.org/~zednenem/>
On Wed, Oct 12, 2005 at 12:05:39AM -0400, David Menendez wrote:
In principle, you could use seperate types to distinguish floats with different rounding modes, but I imagine this would be difficult or annoying to implement.
I think it would make more sense to have different operations for each rounding mode. That matches the reality of the situation more and you don't want to have to do tons of type conversions when you need to operate on something with different rounding modes. of course, then you could declare several 'newtypes' whose default Num instances were of specific rounding modes too. John -- John Meacham - ⑆repetae.net⑆john⑈
John Meacham:
On Wed, Oct 12, 2005 at 12:05:39AM -0400, David Menendez wrote:
In principle, you could use seperate types to distinguish floats with different rounding modes, but I imagine this would be difficult or annoying to implement.
I think it would make more sense to have different operations for each rounding mode. That matches the reality of the situation more and you don't want to have to do tons of type conversions when you need to operate on something with different rounding modes. of course, then you could declare several 'newtypes' whose default Num instances were of specific rounding modes too.
A student of mine designed a special-purpose, pure functional language for block-oriented matrix computations for his PhD. He was very careful to give the language a good design also regarding floating-point computations. His design choice, as regards rounding, was to allow the compiler to choose rounding mode by default (thus allowing more freedom for optimization), while providing a set of special arithmetic operators, with specified rounding modes, to use when more explicit control is needed. He also proposed a special construct "letctrl ... in e", where the "..." are a list of directives telling how to interpret and evaluate the expression e. One possible directive is "RoundingMode = ..." to set the rounding mode locally in e. Other directives control, for instance, whether optimizations like x*0.0 -> 0.0 are allowed in e, whether to force strict evaluation of all subexpressions (so optimizations cannot affect exceptions), to set allowed miminum accuracy, etc. The language has exception handling (including a "handle" construct to catch exceptions), which also takes care of floating-point exceptions. If Haskell ever gets redesigned to give better support for numerical computations, then I think this work is worth a look. Alas, it is not well-published, but the PhD thesis should be possible to order from the Dept. of Information Technology and Microelectronics at KTH. I also have a few copies to give away of someone is interested. Here's the reference: @PHDTHESIS{npd-phd, AUTHOR = {N. Peter Drakenberg}, TITLE = {Computational Structure and Language Design}, SCHOOL = {Dept.\ of Information Technology and Microelectronics, KTH}, ADDRESS = {Stockholm}, YEAR = {2004}, MONTH = sep, NOTE = {TRITA-IMIT-LECS AVH 04:02} } Björn Lisper
In article <20051011060428.B7BC8ACB2@Adric.metnet.navy.mil>, oleg@pobox.com wrote:
To recap, there were two arguments for getArgs to remain with the IO [String] type:
As was argued earlier, it seems the correct thing would be to retype "main": main :: [String] -> IO () or main :: (?args :: [String]) => IO () or type ReturnCode = Int main :: [String] -> IO ReturnCode I sometimes dream of an approach where all system services are provided by implicit parameters supplied to "main": main :: (?stdin :: Handle, ?stdout :: Handle, ?stderr :: Handle, ?filesystem :: FileSystem, ?internets :: [InternetConnection], ?gettime :: IO UTCTime, ... ) => [String] -> IO ReturnCode Of course, this would make a bit more sense if there were a mechanism for applying type contexts to modules, so that they applied to all declared type signatures. -- Ashley Yakeley, Seattle WA
participants (5)
-
Ashley Yakeley -
Bjorn Lisper -
David Menendez -
John Meacham -
oleg@pobox.com