http://www.haskell.org/haskellwiki/Heterogenous_collections
Good grief!
Lisp:
'(123 a "bc" #(d))
Python: [1, "abc", 4.0]http://www.haskell.org/haskellwiki/Heterogenous_collections
Good grief!
Lisp:
'(123 a "bc" #(d))
Python: [1, "abc", 4.0]Trading off easy heterogenous lists buys you a better type checker, more static knowledge, faster programs and so on. It also has the advantage that mapping data into the system can be done safely. The price is that self-describing data (JSON, Transit, Msgpack, ...) is more unwieldy to work with - but a little combinator library, scrap-your-boilerplate-generics, or a reflective code generator goes a long way to solve that problem:
- OCaml: Yojson.Basic.Util - Haskell: Generics, Rank2 polymorphism - Go: JSON unmarshalling
are all examples of how to handle that problem.
Here is another simple problem: divide two integers so that the result has integer type when the numbers divide evenly, otherwise rational type.
Extend your numeric type tower to handle whatever new numeric types I come up with in an internally consistent manner.
In haskell I have number types for things that automatically compute derivatives. I have number types for functions that go to number types so all vector spaces work like numbers as well. I have number types for arbitrary precision floating point numbers, and I can build these things on top of each other.
It is a trade-off. We give up a bit of one thing to get something else.
You can take either side of the deal. I'd argue that the weight of benefit is on the side where we don't have a magic type tower to reason about, but it is a perfectly reasonable stance to say that the thing you want is more important to you.
On the other hand, I can turn around and argue that if what I really want is an ad hoc tower of numeric types I can just make a type for that and work inside it.
data Number = Int Int | Rational Rational | Double Double | Complex (Complex Double) | ...
instance Num Number
Now I can opt into your model. Can you opt into mine?
However, it's not the case that `data Number = ...` gets you everything. In particular, it gives up on the types! :)
For example, in Typed Racket:
-> (: norm : Real Real -> Real)
-> (define (norm x y) (sqrt (+ (sqr x) (sqr y))))
-> (norm -3 12)
- : Real
12.36931687685298
We've proved that the `norm` function always produces `Real` answers, even though `sqrt` might produce complex numbers given negative inputs. The sum type you've given won't let you prove that.
In Python, not only can you "opt in" to those features (in a completely natural way) but those features already exist in the form of highly popular libraries:
NumPy - http://www.numpy.org/
SymPy - http://www.sympygamma.com/input/?i=diff%28x**2%29
mpmath - http://mpmath.org/
...and the story is similar with Common Lisp, whose generic functions dispatch on function parameter type (just like Haskell allows with type classes), allowing you to create libraries for vector arithmetic, symbolic algebra, and arbitrary precision floating point math, again in a completely natural style.
The best forms of language advocacy probably involve showing the great things you've done with a language and forgetting about comparisons to the competition. Build it, and they will come.
Great things the parent has done with the language:
For your first problem, C doesn't help in the general case. If I got a JSON package, I don't know at compile time what the types are going to be, so I don't know how to lay out the struct.
If I don't know anything about the types, the only things I can (safely) do are put them in untyped collections or convert them to strings. I can't add them - they might not be numeric. I can't convert them to uppercase - they might not be strings. (I can do all those things if you're going to return a Maybe - and maybe that's the right approach if you got a JSON package over the wire.)
kazinatorDiv x y = case x `divMod` y of
(q, 0) -> Left q
_ -> Right (x % y)In C++, for example, I might have a “vector<unique_ptr<GameObject>>” containing instances of various subclasses of “GameObject” such as “Player” or “Enemy”. “GameObject” has some virtual member functions such as “update()” and “render()”.
In Haskell I can do exactly the same thing with a “GameObject” type class containing the “update” and “render” methods, and implement “GameObject” for my “Player” and “Enemy” types.
In Python I do a dynamic check on every value in the collection, on every iteration, to see if “update” and “render” are present and callable. It just seems wasteful because you know that information ahead of time—a non–game object should never end up in that collection, so why waste brain power and computing power considering the possibility?
There must be some advantage I’m not seeing here.
A program that compiles in Haskell can still break, consider:
main = do
print $ head []I can't see any reasonable definition of "break" for this context for which that is valid; its exactly the same kind of breakage that strong type checking reduces the incidence of in Haskell compared to dynamic languages -- runtime errors resulting from a function call not being able to complete without error for all inputs that the language will allow to be passed to it.
> Taking the head of an empty list is no more a break than dividing something by zero.
I agree; both are clearly examples of things that can cause a program to break.
So just compile and ship; no testing?
What's the longest you've maintained something?
So I don't think your observation answers the GP's point at all.
There was some computation which produced a wrong result, which was detected, handled and diagnosed.
In what development tool does this never happen?
for x in [1.0, "abc", True]:
print(x)
Of course, all the legitimate things you can do with it illustrate ways in which the collection isn't really heterogenous, its just as collection of elements which have different types that are all within the same sum type.The thing is that in dynamic languages, there's one universal sum type, and every function effectively takes arguments and returns results of that type. In Haskell, you have to plan more for the specific sum types you are going to be using, which in certain situations makes some things more complicated. Of course, it also means that things that would be in documentation or approximately verified in tests can be statically verified by the compiler, so there is a trade off.
That is a reductionistic view, like saying that everything is ultimately ones and zeros. The storage place in a dynamic language, such as a variable, or array element or whatever, is not understood to have a type in that language. The machine language (or whatever) implementation kernel of that language understands that to have a type, but that's in a different language. That kernel knows that, say, if the least significant two bits of the storage location are 00, then it's a pointer to a heap object, and that kind of thing. That code must be written safely: like only if those two bits are 00 must it treat the value as a pointer, and otherwise not. This is all implementation detail, not accessible from the high level language (or not without special escape hatches). The type of the dynamic objects is their dynamic type; and when dynamic languages feature compile-time type inference, that inference works with those types, not with the fictitious "universal sum type".
Maybe, so what?
> like saying that everything is ultimately ones and zeros.
Well, except that saying everything is ultimately ones and zeros isn't (at least in this context) a useful starting point for explaining anything.
> The storage place in a dynamic language, such as a variable, or array element or whatever, is not understood to have a type in that language.
I'm not talking about storage places, I'm talking about the logical design -- a function (regardless of the features of the language) is designed to operate on some set of values and return a result from some set of values. These sets can ar the types I'm interested in -- whether they are implemented as (static) types in a language, or as tags / dynamic types, or whatever, or even if the language had no concepts of types at all (and, in most real languages, and can be true true even withHaskell, as in the head function on lists).
And to the extent that "heterogenous" collections are useful, they are useful specifically because they are not really heterogenous, even if the shared logical type to which the elements must belong is not coherently defined as a discrete "type" within the type system of the language.
(1, "abc", 4.0)If you know what is where in the list, then you semantically have a tuple.
If you don't know what is where in the list, then you need some way of telling things apart. At which point an ADT is frequently the right solution, isn't any sort of advanced technique, and requires very little in the way of boilerplate.