This is the preservation component of the formal concept "type safety". Thus any type safe language must achieve this.
> Objects containing the same values should be equal by default.
This one really depends on a lot of things. You may have abstract types in which case equality should not be implicitly defined. You may also have codata types where you cannot merely compare "value containment" since it's no longer a concept. These are both important things which are sometimes called "objects" and shouldn't be eliminated from languages.
Really, this is a lot more complex than it seems. But at first swing: "structural equality should be everywhere and reference equality isn't a genuine concept" is pretty great.
> Comparing objects of different types is a compile-time error.
This is a-OK so long as it's easy to prove when two dissimilar types are actually the same. In cases where that's not true there are other forms of equality which are useful.
> Once created, objects and collections must be immutable.
This is a little overly strong. Regionalized mutability can be made to be referentially transparent from the outside.
---
But man, yeah, this are tiny nits. This list is great.
I think it's important to distinguish equality from equivalence. There are many cases (e.g., enumeration types) in which equality is a perfectly appropriate definition of equivalence; but there are also many cases (e.g., some codata types) in which equality is an inappropriate definition of equivalence. Therefore, equality-as-equivalence as default behaviour is probably a poor choice— though it should be easy to opt for that behaviour.
As for equality (i.e., intensional equality) itself, I think memberwise equality (or perhaps even equivalence?) is a perfectly reasonable definition. There are times when you really want to discriminate on object identity (in the storage-location sense), but you needn't restrict equality to object identity to allow that.
This is very true. In particular, equality ought to be merely a subset of equivalence. For codata types, as mentioned, equality may not be computable (or well-defined at all), but we can compute equivalences such as bisimulation if we like.
For any type which has a nice equality then merely setting equivalence to equality should be fast, I agree. This is essentially `deriving Eq` in Haskell.
Memberwise equality works for some types, but notably fails when we have abstraction. You can get away with it for (pure) functions on a finite domain if you really want but that's a little hacky---or rather, it involves (implicitly) injecting your function into some kind of container [0] and computing equality there.
As you note, codata types aren't likely to have nice equalities. In these cases they're not going to have memberwise equality, either.
[0] First chapter or so of these lecture notes if I remember correctly, http://lambda-the-ultimate.org/node/4804
Can you give an example of that ? Is that similar to ST Monad ?
The first "issue" is that ``DoSomething accesses x directly rather than through the parameter'', but then goes on to talk about types.
I think this is a big part of why we are bad at programming: A because 6 therefore Go Fish.
I have noticed that when a program is small enough that I can see all of it, then I can reason about all of it, and when I cannot see all of the code, then I cannot. I may resort to note-taking or trying to remember while flipping tabs quickly, or even outright guessing, but I certainly am not reasoning about it.
I do not buy that typing variables, or lambdas, or long method names (like `Equal`), or having "compile time", or `null` or "function signatures" offers anything like the gains had by able to see all of the program, so I'm disinclined to waste energy on what I view as premature optimisation.
You've made a powerful observation about how programmers and human minds in general interface with complex systems. What if you had language semantics that could let you ignore most of your codebase with 100% reliability?
I've had a long career dealing with very complex "enterprise" software for multi-nationals. Semantic tricks that let you make far-reaching deductions and securely ignore most of the code are absolutely vital in contexts like that. It was precisely in the parts of the code where things weren't structured to allow this, that things broke down, and everyone dreaded to check-in code.
I'm disinclined to waste energy on what I view as premature optimisation.
There are certain contexts where one already knows that this sort of thing is not premature optimisation. (Granted, lots of programmers seem to make a fetish of acting like they are in such a context, when they really aren't. Analogous to: http://xkcd.com/538/ )
Another thing I've noticed is that having well documented interfaces between small components is extremely effective at improving isolation time.
Another thing I've noticed is that interfaces with two or fewer arguments (none of which boolean) tend to be used correctly more often than interfaces with a large number of arguments, or booleans.
I would very much enjoy having a conversation about those language semantics. I haven't noticed any language semantics that would be useful in that way, however I have noticed a number of these "disciplines" that are helpful at reducing search time, reducing defect count and increase performance (run-time and programmer).
What exactly are you thinking about?
Good point. There aren't languages that do a complete job of absolutely enforcing such patterns of behavior, but there are environments that do a fairly good job of encouraging them.
Another thing I've noticed is that interfaces with two or fewer arguments (none of which boolean) tend to be used correctly more often than interfaces with a large number of arguments, or booleans.
Goes to the same point. Most of my work with complex stuff was in Smalltalk, and there is practically no enforcement of anything in that language. However, my experience is that it's practical to have codebases that adhere very closely to a number of patterns or coding standards. Also, there were very powerful search facilities in the environment that facilitated powerful deductions about the codebase. Often, work would involve making deductions, figuring out how those could be broken, then quickly searching for these instances. Often enough, the deductions could stand and make debugging and modifications easy.
Your preference in interfaces is also the one preferred by better Smalltalkers. Also, the environment is built up entirely of small components. One path of least resistance is to build new systems along the same lines. (That said, one could still write Fortran and COBOL in Smalltalk, and I had to deal with this professionally.)
Your point about the boolean arguments especially, I can't tell you the number of times I've painstakingly documented the parameter in the /// section of the function (in c#) just to make sure it's clear what passing false actually means.
I should just create an enum{ doTheDance,dontDoTheDance} with two values instead, often thought about it, but for some reason never actually done that.
The next guy will thank you.
It can be hard to believe how useful good types can be.
Do you believe "good types" in a 1000 line program is easier to reason about than a 20 line program?
I certainly believe that there are 20 line programs which are harder to reason about than some 1000 line programs with types. That's a big silly, though, I'd believe the same even without the "with types" caveat.
So, here's what I believe:
* Types impose logical structure on programs which is continuously
checked, high-level, and very human interpretable
* All things being equal, shorter programs are easier to comprehend
than longer programs
* If I were to list the things which aid program comprehensibility
then types + most of the things on the list in this blog post
actually go *far* higher on that list than "short length"
(Edit: I should add, given what came after this point, that "all things being equal" above is, in my eyes, a very difficult hypothesis to satisfy.)I think most professional programmers could write a relational database in under 1000 lines. Arthur did SQL92 in 20:
http://web.archive.org/web/20060505221834/http://kx.com/q/e/...
I think this example is perfectly illustrated: I find it more valuable than anything else I have found to have the entire program all on one screen.
> If I were to list the things which aid program comprehensibility then types + most of the things on the list in this blog post actually go far higher on that list than "short length"
I think there are so many useful and successful applications that "use types" and so many useful and successful applications that do not "use types" that types are probably irrelevant for success.
That many of those useful and successful programs are very large suggests that size has very little (or nothing at all) to do with success.
But comprehensibility?
I cannot imagine what universe would have a large program as being "easier to comprehend" than a short program.
Then we'll merely have to agree to disagree. I personally think this example is remarkable levels of incomprehensible. I'm not going to claim that my personal opinion has any level of generality, though. More power to you if that page of code is easy to comprehend.
> I think there are so many useful and successful applications that "use types" and so many useful and successful applications that do not "use types" that types are probably irrelevant for success.
The existence of examples both positive and negative is insufficient to demonstrate that some cause has no effect. You might strengthen that to say that in your experience the use of types has zero correlation with "useful and successful". I think with a little qualification there we could again get to a point where we'd merely have to agree to disagree.
It's also vital to point out the selection bias incurred by either of our experiences.
> I cannot imagine what universe would have a large program as being "easier to comprehend" than a short program.
I could try to demonstrate examples of both, but it'd just be my opinion as to that selection. So, instead of trying to be comprehensible, I'll just state for posterity:
Offhand, I believe it would be very easy to write a 1000 line (or even larger) implementation of SQL92 which I would find to be far more comprehensible than the example you've linked.
In particular, I believe in this case that attempting to make that code fit on one page is a significant detriment to its comprehensibility.
And again, I'm not going to try to convince you but merely state that as the fact of my opinion.
I can make any program only 1 line long if I remove all the newlines. I don't care if a program fits on one screen if it is unreadable. The example you cite is a poor example of a valuable program as it is on purpose made to be difficult to reason about.
On the other hand, i'm the kind of guy that has read 20 lines of code and missed possible null value bugs. Sucks when you have to tell customers to wait for the next point release before they can use this new feature.
I wonder if you thought I meant "a bug in 20 lines" when I meant a "20 line program".
EDIT: e.g. 20 lines is enough for SQL http://web.archive.org/web/20060505221834/http://kx.com/q/e/...
What is this blasphemy?!
Here's an excellent introduction to K: http://archive.vector.org.uk/art10010830
Most kdb users use Q though, which is something else.
The typical '1000 line program' could be compacted like your program is and all fit onscreen at the same time, if you really wanted. It's not anywhere close to 50 times as much code.
Most of the functions are independent, with the exception of the parse utilities, so it's easier to list the exceptions: cre depends on col, w depends on w0 and w1; v and w are used in e (for tokenising); where (wh) is reused by select (sel) and update (upd). c and a are used for the pull parser.
I'm not proposing "dropping the typing requirement".
I think having the program small and fit on the page is more important than types or no types.
To that end: I don't even want to have the argument about types because I think they're so irrelevant.
I'm sure most professional programmers could write an SQL92 implementation in around 1000 lines; Again, types or no types.
Here's Arthur's SQL written in 20 lines (including lexing and parsing):
http://web.archive.org/web/20060505221834/http://kx.com/q/e/...
Here's Steve Apter's relational (not SQL) database in 16:
But that is exactly what you chose to do in your original post.
I'm happy to see an argument that (having or not having types) is more important than a short program.
but I'm not happy having an argument whether to have a language with types versus one without.
That's what "I think types are irrelevant" means.
It seems like at least some people realise this e.g. https://news.ycombinator.com/item?id=8999077 but if after re-reading my post you disagree, let me know how I could better articulate what I mean.
With regard to your larger issue, almost all programs other than exercises are vastly larger than a screenful, so either the way all software is written is completely wrong, or your preference for screen-sized programs is irrelevant to the real problems of writing real-world software. Just as professional mathematicians need to be able to follow proofs that occupy more than one page, professional programmers need to be able to reason about code that occupies more than one page.
Agreed.
Arthur wrote an SQL in 20 lines of K, and a dynamic window manager in around 60 of C. His programmers editor with syntax highlighting easily fits on a page.
kOS's kernel is under 60 lines of C as well (including annotations; paging, filesystem, syscalls, etc).
Where I work, I wrote an RTB service for digital advertising demand-side platforms- implements HTTP, application protocol decoding, maps and bidding in under 100 lines of C. I fit a video player with VAST+VPAID support in 42 lines of Javascript.
So it follows how programmers write software is completely wrong.
However the inertia will be hard to overcome.
I'm writing this way; I'm finding value writing this way, and at the moment I'm making no other claims other than the shortness of a program is the single best indicator of correctness, performance, and "reasonableness". It's certainly more important than a "types" argument.
While your best counterargument against short programs is inertia, suggesting we shouldn't leave the trees isn't helpful. You should try it. You might like it.
If you had not strayed from that position, I would not have joined the discussion. I happen to agree that there is an inordinate amount of unnecessary complexity in the present code base, but I think it is an orthogonal issue to that being raised by the original article. That is because it seems to me that the main causes of this introduced complexity is developers' poor understanding of the requirements they are trying to satisfy, together with a trial-and-error approach to programming, rather than the language features covered by the original article.
Collections I understand, but what's the point of immutable objects? If you're eschewing mutability, why have objects at all? The whole point of objects is to encapsulate mutable state, together with methods for mutating that state.
If you said Once created, data structures must be immutable I would be more inclined to agree.
Citation needed. I use my objects to hold immutable state with methods that does not mutate that state.
But yeah, otherwise there's no difference, really.
push : ('a queue, 'a) -> 'a queue
pop : 'a queue -> ('a queue, 'a) option
In other words, instead of mandating that state updates in objects occurs via global ambient state simply cause objects to return a new version of themselves with the suitable updated state. All your standard immutability tricks now apply. Objects maintain their meaning through their opaqueness/type-abstraction.That's what Clojure does (and what lots of other functional languages do), only they're not "objects," they're just data structures.
Again, if you're not using mutability or inheritance, then why do you need objects at all? What do objects buy you, compared to data types + functions?
In the absence of those features, isn't an object class really just a module with a data type definition and some functions in it?
In particular, the tip of this iceberg is the difficulty of comparing functions for equality.
Does any of this matter? Maybe not. Structs with recursion can get you 90% of what you want (just ask OCaml). One thing you might miss is the ability to do late binding and open recursion—up to you whether or not you care.
In the absence of objects, that's what `letrec` is for. Scheme, OCaml, and SML all have it. In Haskell `let` is `letrec` by default. Clojure has `letfn`. C has forward declarations. Without these features you wouldn't be able to write mutually recursive functions.
In particular, you don't actually need or want letrec to encode open recursion. That's most of the fun :)
Because the extensible open recursion you're talking about only matters in a situation where you're working with base classes and subclasses having different implementations of the same method name and needing dynamic dispatch to determine which one gets called at runtime.
Clojure's hierarchical multimethod system is an interesting alternative approach to this that I think fulfills the semantics you're talking about. I guess you could argue that this is a sort of immutable object system, it's just not called that. http://clojure.org/runtime_polymorphism
Secondly, "objects" can imply open-recursion which can be used to implement inheritance. That said, inheritance isn't the only thing open recursion is good for and inheritance can be implemented without open recursion.
"Object"s can also imply codata like structures as compared to data like structures. The distinction is blurry to the point of indistinguishability in essentially all languages, but it's important in theorem proving and math and thus shows up in dependently typed languages.
Thirdly, dynamic dispatch is one way to achieve open recursion but it's not the only way. I don't know if dynamic dispatch is useful for things other than open recursion, so offhand I would say "dynamic dispatch => open recursion is being used" but not the other way around.
Clojure's multimethod system, given that it's an implementation of dynamic dispatch, implies open recursion and thus has at least one factor which some people associate with objects. It's immutable (at least to the degree which Clojure is immutable) so I'm willing to call it an "immutable object system", yes. But with all of the caveats I just listed!
Here's a counterexample of runtime polymorphism without objects: http://clojure.org/runtime_polymorphism
I'm not arguing against those two conclusions as a pair, just with the order in which the article presents them.
1. Variables should not be allowed to change their type.
2. Objects containing the same values should be equal by default.
3. Comparing objects of different types is a compile-time error.
4. Objects must always be initialized to a valid state. Not doing so is a compile-time error.
5. Once created, objects and collections must be immutable.
6. No nulls allowed.
7. Missing data or errors must be made explicit in the function signature.
I think the writer also provides a pretty practical definition of what it means for a program to be "reasonable". Really nice job!
EDIT: I pasted the wrong list as dsp1234 pointed out :)