Why do Haskell inferred types in return type polymorphism lead to runtime errors? - haskell

The reason I'd choose to use Haskell is because of its rich type system. This gives me more information at compile-time about my program, helping me have confidence that it is sound.
In addition, it would appear that Haskell is an optimal language in which to approach the expression problem, as Haskell typeclasses can dispatch on return type. (In contrast to Clojure protocols - which can only dispatch on first argument).
When I explore a Haskell polymorphic return value function like read:
read :: (Read a) => String -> a
with the following program:
addFive :: Int -> Int
addFive x = x + 5
main :: IO ()
main = do
print (addFive (read "11"))
putStrLn (read "11")
I get the following result:
Runtime error
...
prog: Prelude.read: no parse
So I appear to be getting a runtime error in a language with a superior type system.
Contrast this with the equivalent code in Clojure:
(defn add-five [x] (+ 5 x))
(println (add-five (read-string "11")))
(println (read-string "11"))
This gives the following result:
16
11
My question is Why do Haskell inferred types in return type polymorphism lead to runtime errors? Shouldn't it pick them up at compile-time?

That runtime error has nothing to do with polymorphism, and everything to do with the fact that the string "11" can't be parsed as a list of characters by the read function.
Here are things that work. Note that "11" can, at runtime, be parsed as an Int and "\"Some More String\"" can, at runtime, be parsed as a string.
print $ 5 + read "11"
print $ "Some string" ++ read "\"Some More String\""
Here are some things that don't work. They don't work because "Not an integer" can not be parsed as an Int and "11" can't be parsed as a string.
print $ 5 + read "Not an integer"
print $ "Some string" ++ read "11"
As was pointed out in the answer to your previous question, the type information has already been inferred at compile time. The read functions have already been selected. Imagine if we had two functions readInt :: String -> Int and readString :: String -> String that were provided for the read function for the Read instances for Int and String respectively. The compiler has already, at compile time, replaced the occurrences of read with the original respective functions:
print $ 5 + readInt "Not an integer"
print $ "Some string" ++ readString "11"
This must have happened at compile time precisely because type information is eliminated at compile time, as was explained in the answer to your previous question.

A part of the issue here is that in Haskell one can define partial functions, i.e., functions which may fail on certain inputs. Examples are read, head, tail. Non-exhaustive pattern matching is the common cause of this partiality, others including error, undefined, and infinite recursion (even if in this case you do not get a runtime error, obviously).
In particular, read is a bit nasty since it requires you to ensure that the string can be parsed. This is usually harder than ensuring that a list is non empty, for instance. One should use a safer variant such as
readMaybe :: Read a => String -> Maybe a
main = do
print $ readMaybe "11" :: Maybe Int -- prints Just 11
print $ readMaybe "11" :: Maybe String -- prints Nothing
Another part of the issue is that polymorphic values (such as read "11") are actually functions in disguise, since they depend on the type at which they are evaluated, as seen in the example above. The monomorphism restriction is an attempt to make them behave more as non-functions: it forces the compiler to find a single type for all the uses of the polymorphic value. If this is possible, the polymorphic value is evaluated only at that type, and the result can be shared in all the uses. Otherwise, you get a type error, even if the code would have been typeable without the restriction.
For example, the following code
main = do
let x = readMaybe "11"
print $ x :: Maybe Int
print $ x :: Maybe Int
parses 11 once if the monomorphism restriction is on, and twice if it is off (unless the compiler is smart enough to do some optimization). By comparison,
main = do
let x = readMaybe "11"
print $ x :: Maybe Int
print $ x :: Maybe String
raises a compile-time type error if the monomorphism restriction is on, and compiles and runs just fine if it is off (printing "Just 11" and "Nothing").
So, there is no clear winner between enabling and disabling the restriction.

The type of read is
(Read a) => String -> a
which implies it (compiler or interpreter, actually) will choose its return type according to the requirement of context.
Therefore, in addFive (read "11"), because addFive requires a Int, the type of read chosen by compiler will be String -> Int; in putStrLn (read "11"), it will be String->String because putStrLn requires a String.
And this choice happens at compile time, which means after compilation, your program sort of equals
main = do
print (addFive (readInt "11"))
putStrLn (readString "11")
But this readString cannot parse its argument "11" as a string, so it crash at run time.
The fix of this problem is simple:
main = do
print (addFive (read "11"))
putStrLn (read "\"11\"")

Related

Is any Atom data type in Haskell?

When I read/show String I get/expect the result to be quoted. If I want to omit quotes, I can implement some type like:
newtype Atom = Atom String deriving (Eq, Ord)
instance Read Atom where ...
instance Show Atom where ...
But my question is: does such type already exist somewhere in libraries, in the base, may be? I just found some Data.Atom but seems it something different (XML, etc).
No. If you find yourself wanting this, it means you're using Show in an unintended way. The idea of show is that is should always yield valid Haskell code. The Show String instance guarantees this, by safely quoting and escaping the contents. Short of that, you would of course in general not get Haskell code. Now, sure there are applications where you want to display something and don't care about whether it's Haskell code, but that shouldn't be show then.
Usually, if you have a string anyway, you should just use putStrLn directly.
Since Read and Show are for converting data to/from strings, you can just use the id function :)
But seriously, a String is already the output of its own read or show, if you don't want it quoted.
Say, for example, to print any other value (or a String, if you want it to appear quoted), you do:
putStrLn (show "Hello") -- prints "Hello", with quotes.
whereas, to print a string unquoted, you can simply do:
putStrLn "Hello" -- prints Hello, unquoted.
Because of this, I don't think there is any utility for this (it is trivial, after all); in fact, your Atom instances would simply be:
instance Read Atom where read = Atom
instance Show Atom where show (Atom s) = s
Depending on what you're doing, you may want to inspect the type and use print for non-strings and putStrLn for strings like this:
import Data.Typeable
printAny :: (Show a, Typeable a) => a -> IO ()
printAny a = case cast a of
Just str -> putStrLn str
Nothing -> print a
Then, as desired:
*Main> printAny "no quotes"
no quotes
*Main> printAny (10 :: Int)
10

In Haskell, if a function returns a "Maybe a" type just so it is safe and total, how is it useful anymore?

So I have to define a safe version of the head function that would not throw an error when [] is passed as the argument. Here it is:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
But now, is this function still of any use? Because suppose that type "a" is a Int, then you can add two objects of type Int, but you can't add two objects of type "Maybe Int".
As it was mentioned in comments, you can actually add two Maybes. I just wanted to give another point of view on that.
Yes, you can't directly apply (+) to Maybe Ints, but you can upgrade it to another function that is able to do so automatically.
To upgrade unary function (like (+1)) you write fmap (+1) maybeInt or (+1) <$> maybeInt. If (+1) had type Int -> Int, the fmap (+1) expression has type Maybe Int -> Maybe Int.
Upgrading bin-or-more-ary functions is a bit more complex syntax-wise: (+) <$> maybeInt <*> maybeInt or liftA2 (+) maybeInt maybeInt. Again, here we promote (+) :: Int -> Int -> Int to liftA2 (+) :: Maybe Int -> Maybe Int -> Maybe Int.
Handling Maybes this way allows you to build up a computation that works with Maybes out of pure functions and defer checking for Nothing. Or even avoid that if you eventually plug it into another function that takes Maybe as argument.
Of course, you can use fmap and liftAs on any Applicative, not just Maybe.
"Just" is one such function. Here's how you can use its result (for the ghci REPL):
import Data.Foldable (sequenceA_)
let writeLn = putStrLn . show
let supposedlyUnusable = writeLn <$> Just 0
sequenceA_ supposedlyUnusable
which prints 1 or we can continue to try the other interesting example - using the Nothing case
let supposedlyUnusable = writeLn <$> Nothing
sequenceA_ supposedlyUnusable
which doesn't print anything.
That's a complete program which works even for other instances of Traversable or Foldable where you couldn't do a case analysis on the Maybe value. <$> is the key that lets you apply a function to whatever's contained in the Maybe or any Functor and if you have two Maybes (or two of the same Applicative) you can use the pattern fn <$> applicative_a <*> applicative_b which is like fn a b but where a and b are wrapped up things like Maybe values.
So that leaves a couple of remaining ways to use a Maybe that I can think of, all of which use case analysis:
let {fn (Just n) = Just $ 1 + n; fn Nothing = Nothing}
fn v
-- but that was just a messy way of writing (1+) <$> v
...
let fn v = case v of {Just n -> Just $ 1 + n; Nothing -> Nothing}
-- and that's the same program with a different syntax
...
import Data.Maybe (fromMaybe)
fromMaybe someDefault v
-- and that extracted the `value` from `v` if we had `Just value` or else gave us `someDefault`
...
let {fn (Just n) = writeLn n; fn Nothing = putStrLn "No answer"}
-- this one extracts an action but also provides an action when there's nothing
-- it can be done using <$> and fromMaybe instead, but beginners tend to
-- find it easier because of the tutorials that resulted from the history
-- of the base library's development
let fn v = fromMaybe (putStrLn "No answer") (writeLn <$> v)
oooh, oooh! This one's neato:
import Control.Applicative
let v = Just 0 -- or Nothing, if you want
let errorcase = pure $ putStrLn "No answer"
let successcase = writeLn <$> v
sequenceA_ $ successcase <|> errorcase
-- that uses Alternative in which Maybe tries to give an answer preferring the earliest if it can
of course we also have the classic:
maybe (putStrLn "No answer") writeLn v
Safety comes with a cost. The cost is normally extra code, for avoiding error situations. Haskell has given us the way to avoid this at the compile time rather than at run time.
Let me explain with examples from other languages. Though I won't name any language, but it would be apparent which languages I am talking about. Please be sure that all languages are great in their ways, so do not take this as I am finding fault in other language.
In some languages you have pointers and the way you will do safeHead is to return either int pointer or null pointer. You will have to de-reference pointer to get the value and when you de-reference null pointer you will get error. To avoid this, extra code will be needed to check for null pointer, and do something when it is null.
In some dynamic languages, you have variables assigned to null. So in above example your variable could be type int or it could be null. And what will happen if you add null to int? Most probably undefined situation. Again special handling needs to be done for the null case.
In Haskell too you will have to do the same, you will have to guard the null situation with extra code. So what's the difference? The difference in Haskell is doing it at the compile time and not at the run time.* i.e. the moment you have this kind of code along with your definition of safeHead, p = safeHead xs + safeHead ys, the code will give error at the compile time. You will have to do something more for addition if type Maybe Int. You can write your function for adding two or multiple Maybe Ints or create newype for Maybe Int and overload + or do something as mentioned in other answers.
But whatever you do, you do it before unit testing. Definitely much before it goes on production. And earlier the error is caught lesser is the cost. That's where the advantage of type safe Haskell comes in handy.
* There could be mechanism in other languages to handle this at compile time.

Understanding readMaybe (Text.Read)

I'm currenlty learning Haskell and have questions regarding this example found in Joachim Breitner's online course CIS194:
import Text.Read
main = putStrLn "Hello World. Please enter a number:" >>
getLine >>= \s ->
case readMaybe s of -- why not `readMaybe s :: Maybe Int` ?!
Just n -> let m = n + 1 in
putStrLn (show m)
Nothing -> putStrLn "That’s not a number! Try again"
The code does exactly what expected, that is it returns an integer +1 if the input is an integer and it returns "That’s not a number! Try again" otherwise (e.g. if the input is a Double).
I don't understand why readMaybe s only returns Just n if n is of type Int. The type of readMaybe is readMaybe :: Read a => String -> Maybe a and therefore I thought it would only work if the line read instead:
case readMaybe s :: Maybe Int of
In fact if I just prompt > readMaybe "3" in ghci, it returns Nothing, whereas > readMaybe "3" :: Maybe Int returns Just 3.
To sum up, my question is the following: how does the compiler now that s is parsed to an Int and not something else (e.g. Double) without the use of :: Maybe Int? Why does it not return Nothing everytime ?
I hope my question was clear enough, thanks a lot for your help.
TL;DR: The context of readMaybe s tells us that it's a Num a => Maybe a, defaulting makes it a Maybe Integer.
We have to look at all places where the result of readMaybe is used to determine its type.
We have
Nothing, which doesn't tell us aynthing about a
Just n, and n is used in the context m = n + 1.
Since m = n + 1, we now know that n's type must be an instance of Num, since (+) :: Num a => a -> a -> a and 1 :: Num a => a. At this point the type isn't clear, therefore it gets defaulted:
4.3.4 Ambiguous Types, and Defaults for Overloaded Numeric Operations
topdecl -> default (type1 , ... , typen) (n>=0)
A problem inherent with Haskell -style overloading is the possibility of an ambiguous type. For example, using the read and show functions defined in Chapter 10, and supposing that just Int and Bool are members of Read and Show, then the expression
let x = read "..." in show x -- invalid
is ambiguous, because the types for show and read,
show :: forall a. Show a =>a ->String
read :: forall a. Read a =>String ->a
could be satisfied by instantiating a as either Int in both cases, or Bool. Such expressions are considered ill-typed, a static error.
We say that an expression e has an ambiguous type if, in its type forall u. cx =>t, there is a type variable u in u that occurs in cx but not in t. Such types are invalid.
The defaults defined in the Haskell report are default (Integer, Double), e.g. GHC tries Integer first, and if that doesn't work it tries to use Double.
Since Integer is a valid type in the context m = n + 1, we have m :: Integer, therefore n :: Integer, and at last readMaybe s :: Maybe Integer.
If you want to disable defaults, use default () and you'll be greeted by ambiguous types errors, just as you expected.
There indeed some underlying magic, due to how type inference works.
Here's a simpler example, run inside GHCi:
> print (1 :: Integer)
1
> print (1 :: Float)
1.0
Prelude> print 1
1
In the last line, 1 is a polymorphic value of type Num a => a, i.e. a value inside any numeric type like Integer and Float. If we consider that value inside type Integer, we print it as "1". If we consider it as a Float, we print it as "1.0". Other numeric types may even have different print formats.
Still, GHCi in the last line decides that 1 is an Integer. Why?
Well, it turns out that the code is ambiguous: after all 1 could be printed in different ways! Haskell in such cases raises an error, due to the ambiguity. However, it makes an exception for numeric types (those inc lass Num), to be more convenient to program. Concretely, when a numeric type is not precisely determined by the code, Haskell uses its defaulting rules, which specify which numeric types should be used.
GHC can warn when defaulting happens, if wanted.
Further, the types are propagated. If we evaluate
case readMaybe s of
Just x -> let z = x + length ['a','z']
in ...
GHC knows that length returns an Int. Also, (+) operates only on arguments of the same type, hence x has to be an Int as well. This in turns implies that the call readMaybe s has to return Maybe Int. Hence, the right Read instance for Ints is chosen.
Note how this information is propagated backwards by the type inference engine, so that the programmer does not have to add type annotations which can be deduced from the rest of the code. It happens very frequently in Haskell.
One can always be explicit, as in
readMaybe s :: Maybe Int
-- or, with extensions on, one can mention the variable part of the type, only
readMaybe s # Int
If you prefer, feel free to add such annotations. Sometimes, they make the code more readable since they document your intent. Whoever reads the code, can immediately spot which Read instance is being used here without looking at the context.

Getting 'a' value from 'Maybe a' return type in Haskell

I have a Haskell function eval :: WExp -> Memory -> WValue with a bunch of different instances of itself for different cases. For now, knowledge about WExp, Memory, and WValue is not relevant. My problem is that, for a specific instance of eval, I am using a lookup function, which takes the parameter of eval (a string in this case) searches a list of key-value pairs for that string. Note that this lookup function is not the one included in the Prelude; it is self-defined within the .hs file. If the string is found, the value associated with it is returned, but if it is not found, Nothing is returned. Because of the Nothing case, the type of lookup is actually Maybe a, where a would be a WValue in this case. Because eval would return a Maybe WValue, the compiler obviously complains that the type is not WValue.
I thought that there might be some kind of general method to extract the a value from any function that returns Maybe a.
Do this
do
input <- getUserInput
result <- lookup input structure
case result of
Just a -> putStrLn $ "I'm so happy you chose "++show a++"."
Nothing -> putStrLn $ "So sorry; "++input++" is not a valid option."
Don't do this
do
input <- getUserInput
result <- lookup input structure
case result of
Just a -> putStrLn $ "I'm so happy you chose "++show a++"."
Nothing -> error $ input ++ " is not a valid option."
This is bad because your program just goes splat if the user input is wrong.
Really don't do this
There is a function called fromJust that attempts to pull a value out of a Maybe and throws an error if it finds Nothing. It looks like
fromJust :: Maybe a -> a
fromJust (Just a) = a
fromJust Nothing = error "Oops, you goofed up, fool."
This makes it hard to see what went wrong.
And really, really don't do this
But if you want to play with fire, you can try it just for fun. This will attempt to get a value out of a Maybe and crash real hard if it finds Nothing. By "crash real hard" I mean you'll get a segmentation fault if you're lucky, and you'll publish your private keys on the web if you're not.
{-# LANGUAGE GADTs, DataKinds, KindSignatures #-}
{-# OPTIONS_GHC -fno-warn-unused-binds #-}
module Unsafe.FromJust (unsafeFromJust) where
-- Clear sign of bad news
import Unsafe.Coerce (unsafeCoerce)
-- This creates a "closed kind" with types
-- 'JustType and 'NothingType. You could just
-- define datatypes called JustType and NothingType,
-- but this makes the intent clearer.
data MaybeType = JustType | NothingType
data M (t::MaybeType) a where
-- The order of these constructors must not
-- be changed, because this type must look,
-- at runtime, exactly like a Maybe
N :: M 'NothingType a
J :: a -> M 'JustType a
-- A safe sort of fromJust for M.
fromJ :: M 'JustType a -> a
fromJ (J a) = a
-- Really, seriously unsafe.
unsafeFromJust :: Maybe a -> a
unsafeFromJust m = fromJ (unsafeCoerce m)
The function you are looking for is maybe defined in Prelude.
You need to decide on what to return if the expression is Nothing. Lets say you want to get empty string "" for Nothing. Then the following will let you get out of Maybe boxes.
Prelude> maybe "" id (Just "hello")
"hello"
Prelude> maybe "" id (Nothing)
""
If you know that the lookup is successful, and that the Maybe a is actually Just a, you can simply pattern match:
let (Just val) = lookup ...
and there you have your val::a out of your Maybe a. Note that this is unsafe code which will ungracefully throw an error if lookup returns a Nothing.
Well, you got yourself into a quagmire because the type of your lookup says that it could fail. Haskell forces you in this case to deal with the possibility that such a failure will occur. This is the case if lookup returns Nothing.
If you are really sure that lookup never fails (maybe because you preprocessed and type-checked the program, or you really trust it :) ) you could use fromJust from Data.Maybe. Note that is is really just a band-aid solution because fromJust will produce a (Haskell) runtime error on its own if called with Nothing.

Type inference interferes with referential transparency

What is the precise promise/guarantee the Haskell language provides with respect to referential transparency? At least the Haskell report does not mention this notion.
Consider the expression
(7^7^7`mod`5`mod`2)
And I want to know whether or not this expression is 1. For my safety, I will do perform this twice:
( (7^7^7`mod`5`mod`2)==1, [False,True]!!(7^7^7`mod`5`mod`2) )
which now gives (True,False) with GHCi 7.4.1.
Evidently, this expression is now referentially opaque. How can I tell whether or not a program is subject to such behavior? I can inundate the program with :: all over but that does not make it very readable. Is there any other class of Haskell programs in between that I miss? That is between a fully annotated and an unannotated one?
(Apart from the only somewhat related question I found on SO there must be something else on this)
I do not think there's any guarantee that evaluating a polymorphically typed expression such as 5 at different types will produce "compatible" results, for any reasonable definition of "compatible".
GHCi session:
> class C a where num :: a
> instance C Int where num = 0
> instance C Double where num = 1
> num + length [] -- length returns an Int
0
> num + 0 -- GHCi defaults to Double for some reason
1.0
This looks as it's breaking referential transparency since length [] and 0 should be equal, but under the hood it's num that's being used at different types.
Also,
> "" == []
True
> [] == [1]
False
> "" == [1]
*** Type error
where one could have expected False in the last line.
So, I think referential transparency only holds when the exact types are specified to resolve polymorphism. An explicit type parameter application à la System F would make it possible to always substitute a variable with its definition without altering the semantics: as far as I understand, GHC internally does exactly this during optimization to ensure that semantics is unaffected. Indeed, GHC Core has explicit type arguments which are passed around.
The problem is overloading, which does indeed sort of violate referential transparency. You have no idea what something like (+) does in Haskell; it depends on the type.
When a numeric type is unconstrained in a Haskell program the compiler uses type defaulting to pick some suitable type. This is for convenience, and usually doesn't lead to any surprises. But in this case it did lead to a surprise. In ghc you can use -fwarn-type-defaults to see when the compiler has used defaulting to pick a type for you. You can also add the line default () to your module to stop all defaulting.
I thought of something which might help clarify things...
The expression mod (7^7^7) 5 has type Integral a so there are two common ways to convert it to an Int:
Perform all of the arithmetic using Integer operations and types and then convert the result to an Int.
Perform all of the arithmetic using Int operations.
If the expression is used in an Int context Haskell will perform method #2. If you want to force Haskell to use #1 you have to write:
fromInteger (mod (7^7^7) 5)
This will ensure that all of the arithmetic operations will be performed using Integer operations and types.
When you enter he expression at the ghci REPL, defaulting rules typed the expression as an Integer, so method #1 was used. When you use the expression with the !! operator it was typed as an Int, so it was computed via method #2.
My original answer:
In Haskell the evaluation of an expression like
(7^7^7`mod`5`mod`2)
depends entirely on which Integral instance is being used, and this is something that every Haskell programmer learns to accept.
The second thing that every programmer (in any language) has to be aware of is that numeric operations are subject to overflow, underflow, loss of precision, etc. and thereby the laws for arithmetic may not always hold. For instance, x+1 > x is not always true; addition and multiple of real numbers is not always associative; the distributive law does not always hold; etc. When you create an overflowing expression you enter the realm of undefined behavior.
Also, in this particular case there are better ways to go about evaluating this expression which preserves more of our expectation of what the result should be. In particular, if you want to efficiently and accurately compute a^b mod c you should be using the "power mod" algorithm.
Update: Run the following program to see how the choice of Integral instance affects the what an expression evaluates to:
import Data.Int
import Data.Word
import Data.LargeWord -- cabal install largeword
expr :: Integral a => a
expr = (7^e `mod` 5)
where e = 823543 :: Int
main :: IO ()
main = do
putStrLn $ "as an Integer: " ++ show (expr :: Integer)
putStrLn $ "as an Int64: " ++ show (expr :: Int64)
putStrLn $ "as an Int: " ++ show (expr :: Int)
putStrLn $ "as an Int32: " ++ show (expr :: Int32)
putStrLn $ "as an Int16: " ++ show (expr :: Int16)
putStrLn $ "as a Word8: " ++ show (expr :: Word8)
putStrLn $ "as a Word16: " ++ show (expr :: Word16)
putStrLn $ "as a Word32: " ++ show (expr :: Word32)
putStrLn $ "as a Word128: " ++ show (expr :: Word128)
putStrLn $ "as a Word192: " ++ show (expr :: Word192)
putStrLn $ "as a Word224: " ++ show (expr :: Word224)
putStrLn $ "as a Word256: " ++ show (expr :: Word256)
and the output (compiled with GHC 7.8.3 (64-bit):
as an Integer: 3
as an Int64: 2
as an Int: 2
as an Int32: 3
as an Int16: 3
as a Word8: 4
as a Word16: 3
as a Word32: 3
as a Word128: 4
as a Word192: 0
as a Word224: 2
as a Word256: 1
What is the precise promise/guarantee the Haskell language provides with respect to referential transparency? At least the Haskell report does not mention this notion.
Haskell does not provide a precise promise or guarantee. There exist many functions like unsafePerformIO or traceShow which are not referentially transparent. The extension called Safe Haskell however provides the following promise:
Referential transparency — Functions in the safe language are deterministic, evaluating them will not cause any side effects. Functions in the IO monad are still allowed and behave as usual. Any pure function though, as according to its type, is guaranteed to indeed be pure. This property allows a user of the safe language to trust the types. This means, for example, that the unsafePerformIO :: IO a -> a function is disallowed in the safe language.
Haskell provides an informal promise outside of this: the Prelude and base libraries tend to be free of side effects and Haskell programmers tend to label things with side effects as such.
Evidently, this expression is now referentially opaque. How can I tell whether or not a program is subject to such behavior? I can inundate the program with :: all over but that does not make it very readable. Is there any other class of Haskell programs in between that I miss? That is between a fully annotated and an unannotated one?
As others have said, the problem emerges from this behavior:
Prelude> ( (7^7^7`mod`5`mod`2)==1, [False,True]!!(7^7^7`mod`5`mod`2) )
(True,False)
Prelude> 7^7^7`mod`5`mod`2 :: Integer
1
Prelude> 7^7^7`mod`5`mod`2 :: Int
0
This happens because 7^7^7 is a huge number (about 700,000 decimal digits) which easily overflows a 64-bit Int type, but the problem will not be reproducible on 32-bit systems:
Prelude> :m + Data.Int
Prelude Data.Int> 7^7^7 :: Int64
-3568518334133427593
Prelude Data.Int> 7^7^7 :: Int32
1602364023
Prelude Data.Int> 7^7^7 :: Int16
8823
If using rem (7^7^7) 5 the remainder for Int64 will be reported as -3 but since -3 is equivalent to +2 modulo 5, mod reports +2.
The Integer answer is used on the left due to the defaulting rules for Integral classes; the platform-specific Int type is used on the right due to the type of (!!) :: [a] -> Int -> a. If you use the appropriate indexing operator for Integral a you instead get something consistent:
Prelude> :m + Data.List
Prelude Data.List> ((7^7^7`mod`5`mod`2) == 1, genericIndex [False,True] (7^7^7`mod`5`mod`2))
(True,True)
The problem here is not referential transparency because the functions that we're calling ^ are actually two different functions (as they have different types). What has tripped you up is typeclasses, which are an implementation of constrained ambiguity in Haskell; you have discovered that this ambiguity (unlike unconstrained ambiguity -- i.e. parametric types) can deliver counterintuitive results. This shouldn't be too surprising but it's definitely a little strange at times.
A another type has been chosen, because !! requires an Int. The full computation now uses Int instead of Integer.
λ> ( (7^7^7`mod`5`mod`2 :: Int)==1, [False,True]!!(7^7^7`mod`5`mod`2) )
(False,False)
What you think this has to do with referential transparency? Your uses of 7, ^, mod, 5, 2, and == are applications of those variables to dictionaries, yes, but I don't see why you think that fact makes Haskell referentially opaque. Often applying the same function to different arguments produces different results, after all!
Referential transparency has to do with this expression:
let x :: Int = 7^7^7`mod`5`mod`2 in (x == 1, [False, True] !! x)
x is here a single value, and should always have that same single value.
By contrast, if you say:
let x :: forall a. Num a => a; x = 7^7^7`mod`5`mod`2 in (x == 1, [False, True] !! x)
(or use the expression inline, which is equivalent), x is now a function, and can return different values depending on the Num argument you supply to it. You might as well complain that let f = (+1) in map f [1, 2, 3] is [2, 3, 4], but let f = (+3) in map f [1, 2, 3] is [4, 5, 6] and then say "Haskell gives different values for map f [1, 2, 3] depending on the context so it's referentially opaque"!
Probably another type-inference and referential-transparency related thing is the „dreaded“ Monomorphism restriction (its absence, to be exact). A direct quote:
An example, from „A History of Haskell“:
Consider the genericLength function, from Data.List
genericLength :: Num a => [b] -> a
And consider the function:
f xs = (len, len)
where
len = genericLength xs
len has type Num a => a and, without the monomorphism restriction, it could be computed twice.
Notice that in this case types of both expressions are the same. Results are too, but the substitution isn't always possible.

Resources