I. ZipList and List
In List scenario, xs <*> ys means taking functions from left side xs to apply to each item in right side ys, there are two implementation methods:
-
Cartesian product
-
Zipper-style one-to-one pairing
Corresponding to [] and ZipList respectively, for example:
import Control.Applicative;
-- Cartesian product
> [(+2), (*2), (/2)] <*> [1, 2, 3]
[3.0,4.0,5.0,2.0,4.0,6.0,0.5,1.0,1.5]
-- Zipper-style pairing
> getZipList $ ZipList [(+2), (*2)] <*> ZipList [1..]
[3,4]
Cartesian product can only be used for finite length Lists, while zipper-style pairing is also applicable to infinite length List scenarios. For <*>, both implementations are acceptable, but [] cannot simultaneously have two different Applicative implementations, so ZipList was created, letting it implement Applicative in zipper pairing way
P.S. The <*> mentioned here is behavior defined by Applicative class, see [Functor and Applicative_Haskell Notes 7](/articles/functor 与 applicative-haskell 笔记 7/) for details
II. newtype
ZipList was produced due to this scenario, essentially a wrapper around List, definition is as follows:
newtype ZipList a = ZipList { getZipList :: [a] }
deriving ( Show, Eq, Ord, Read, Functor
, Foldable, Generic, Generic1)
(From Control.Applicative)
Through newtype keyword, create a new one (ZipList) based on existing type ([]), then rewrite its interface implementation:
instance Applicative ZipList where
pure x = ZipList (repeat x)
liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys)
P.S. Here only implemented liftA2, without <*> appearing, because Applicative has minimal complete definition constraint:
A minimal complete definition must include implementations of pure and of either <*> or liftA2. If it defines both, then they must behave the same as their default definitions:
(<*>) = liftA2 idliftA2 f x y = f <$> x <*> y
Pre-defined the relationship between these two functions, so implementing one is enough (the other can be automatically generated according to relationship)
So, what does newtype actually do?
Actually, what newtype does is just create new type, wrap existing type
In similar scenarios, in JS, we would do this:
class ThisType {
constructor(value) {
this.value = value;
}
['<*>']() {
console.log('Cartesian product');
}
};
class ThatType {
constructor(...args) {
this.originalValue = new ThisType(...args);
}
getOriginalType() {
return this.originalValue;
}
['<*>']() {
console.log('Zipper pairing');
}
};
// test
let thisOne = new ThisType(1);
thisOne['<*>'](); // Cartesian product
console.log(thisOne); // ThisType?{value: 1}
let thatOne = new ThatType(2);
thatOne['<*>'](); // Zipper pairing
console.log(thatOne.getOriginalType()); // ThisType?{value: 2}
Create new type (ThatType), wrap original type (ThisType), provide different <*> implementation
The two are just simple dependency, no inheritance relationship, so types created through newtype don't automatically have all methods of original type (won't automatically get typeclass implemented by original type either). In terms of type, the two are completely independent different types, so:
> [3] ++ [1, 2]
[3,1,2]
> type IntList = [Int]
> [3] ++ ([1, 2] :: IntList)
[3,1,2]
> (ZipList [3]) ++ (ZipList [1, 2])
<interactive>:109:1: error:
? Couldn't match expected type '[a]'
with actual type 'ZipList Integer'
? In the first argument of '(++)', namely 'ZipList [3]'
...
Unlike alias types created by type that can be equivalently exchanged with original type, new types created by newtype are completely different things from original type, the only connection is that new type internally actually operates on original type (by holding original type instance reference), through this way achieve extension/enhancement of original type on outer layer
Syntax Requirements
From syntax function perspective, newtype is same as data, both used to create new types, but newtype has more restrictions:
data can only be replaced with newtype if the type has exactly one constructor with exactly one field inside it.
Requires types declared by newtype can only have one value constructor, and this value constructor can only have one parameter (field). Besides this, no difference from data keyword
P.S. About value constructors and parameters, see [Types_Haskell Notes 3](/articles/类型-haskell 笔记 3/#articleHeader7)
III. Compare type and data
| Keyword | Function | Application Scenario |
|---|---|---|
data | Define own (data) type | When wanting to define completely new type |
type | Give existing type an alias,得到的 thing is completely equivalent to original type, can be unconditionally exchanged/mixed | When wanting type signature to be clearer (semantic) |
newtype | Wrap existing type into a new type,得到的 type is different from original type, cannot be exchanged/mixed | When wanting existing type to have a different interface (typeclass) implementation |
IV. newtype and Lazy Evaluation
Most calculations in Haskell are lazy (minority refers to strict versions like foldl', Data.ByteString, etc.), that is, calculations only happen when不得不算
Lazy evaluation generally looks very intuitive (don't calculate what doesn't need to be calculated first), but specially, type-related scenarios have implicit calculations (not very intuitive)
undefined
undefined represents calculations that will cause errors:
> undefined
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
undefined, called at <interactive>:12:1 in interactive:Ghci1
Can be used to test laziness (whether calculation actually executed), for example:
> head [1, undefined, 3, undefined, undefined]
1
> let (a, _) = (1, undefined) in a + 1
2
Specially, pattern matching itself during function calls needs calculation, regardless of whether matching result needs to be used, for example:
sayHello (_, _) = "hoho"
> sayHello undefined
"*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
undefined, called at <interactive>:37:10 in interactive:Ghci17
While this form below won't be calculated:
sayHello _ = "hoho"
> sayHello undefined
"hoho"
Difference between the two is, for former, need to do some basic calculations to see which value constructor of Tuple should be used, latter doesn't need this
But strangely, Tuple clearly only has one value constructor (no need to "see which value constructor of Tuple should be used"):
data () = ()
We know there's no need to check which value constructor of Tuple should be used, but Haskell doesn't know, because according to convention, data types defined by data keyword can have multiple value constructors, even if only one is declared, it also needs to find out. So, thought of something?
newtype. It clearly agrees there's only one value constructor (and this value constructor only has one parameter), might as well try:
newtype MyTuple a b = MyTuple {getTuple :: (a, b)}
> sayHello (MyTuple _) = "hh"
> sayHello undefined
"hh"
Indeed so, Haskell is smart enough, when clearly knowing there don't exist multiple value constructors, no longer does unnecessary calculations
No comments yet. Be the first to share your thoughts.