Hmm, I think I really should look at Haskell someday. It seems to come up a lot in these language discussions.
Rust has a bottom type though. We spell it !
. Anyway, all languages that don't have a compile-term termination checker have to contend with | on some level.
Someone confirmed it is ad hoc polymorphism, and my reply which is relevant in this thread also:
I am surprised afaics the ad hoc polymorphism is not mentioned any where in the documentation. And the Expression Problem nor the Wikipedia entry on Composition over Inheritance, are also ostensibly not mentioned in the documentation. For me, if considering Rust as potentially a better "high-level" language, the ad hoc polymorphism in a language which does not have Haskell's coinductive type system, seems to be unavailable in any other C/C++ derivative (potentially mainstream) language?
I'm suggesting the documentation maybe could be improved to proactively explain to incoming OOP (aka subclassing) converts, to make an argument for why they typically don't want to be using the anti-pattern of OOP virtual methods, and instead using late binding dispatch at the call site, instead of at the declaration site. In other words, ad hoc polymorphism un-conflates (makes) interface from (orthogonal to) data type, and the binding of the interface to a data type occurs at the function call site, not at the data type, interface, nor function declaration sites. Of course there are some tradeoffs, but the inflexibility of premature binding is removed.
For a mainstream high-level language, I am starting to contemplate if I am wishing Rust's ad hoc polymorphism was available in a strongly type language that had no verbosity GC and didn't basically force on us by default the noisy and complex type system of modeling lifetimes and memory (which apparently even infects generics with the
'a
syntax ... I haven't learned that yet though). The lifetimes and memory allocation feels too heavy (a PITA) for a language that most programmers would want to use most of the time. Sometimes you want that control, but always by default? And a mainstream language without first-class (i.e. not a library) async/await is becoming an anathema.
I think Haskell is an excellent choice for "high-level" situations. It
takes some doing to learn itAnd maybe too "high level" because you basically have to learn category
theory before you can do some things.
How much Haskell have you written? My guess is "not much", or maybe "none".
I've written a LOT of it and what you say above is absolutely not true. You
simply learn the 'do' construct and why and when you need it, which is not
difficult.
Not to disparage Haskell because I agree it is really great for its target
use cases and demographics. That is if you want to learn what a Monad is
just to do I/O and any imperative style programming.
Which takes a short period of time with Paul Hudak's "Gentle" introduction,
or "Learn You a Haskell". It's comparable to learning any programming
language. The only thing that makes learning Haskell more of a challenge
than, say, Python, is that Haskell is different in some important ways
(functional language, lazy evaluation) from more mainstream languages, and
so requires a somewhat different mindset. Not unlike ownership, borrows,
and lifetimes, but far easier, in my experience (though I will concede that
the difficulty I encountered with Rust, and I am far from alone, was, at
least in part, due to the state of the documentation at the time I
attempted to use the language; hopefully, that is a temporary situation).
And no parenthesis grouping functional call arguments, so you need to
memorize the definition site of the functions in order to group the
function arguments in order to read the code.
Again, untrue. It is always clear from the code what function-call
arguments are, otherwise it wouldn't compile!
Haskell seems to be for a mathematical mind and it inverts the type system
to coinduction thus populating every type with Bottom, i.e. the
conjunction of all types
https://existentialtype.wordpress.com/2011/04/24/the-real-point-of-laziness/.
Whereas many programmers want to think more inductively (where Any is the
Top disjunction of all types) and imperatively as they accustomed to
coming from C, C++, Java, Python, Javascript, etc.
You are again demonstrating what I am guessing is a lack of real experience
with Haskell, and therefore you don't know the difference between theory
and practice. I have written a lot of code in all the languages you mention
(and many you haven't -- I've been doing this for a very long time) and
none of the theory you spout above was remotely a consideration. It's for
academicians to debate and write papers about, but it doesn't come up in
the real world.
The fact is that today's Haskell is a highly developed programming
environment with a large library of useful tools and an amazing compiler
(GHC). The language is very expressive -- used correctly, Haskell programs
are very concise. And much debugging is moved to compile-time (avoiding
some less efficient run-time debugging), because of the strong typing and
excellent compiler diagnostics. It's a great way to quickly develop correct
code that performs well.
My interest in Rust was the possibility that it could serve in places where
I presently use C and that it shares some of the characteristics that I
value in Haskell. I hope to come back to it at some point when the
documentation is improved and find that it is useful to me. But I do not
think that Rust and Haskell are comparable languages, any more than C and
Python are comparable. Their areas of appropriate applicability are quite
disjoint. This is because there is an inevitable trade-off between
ease-of-use and performance. Because of the power of today's hardware, we
can do a lot of useful things even with interpreted languages, and with
compiled languages that deliver less-than-maximal performance, like Haskell
and Scheme. But when maximum performance really is a top priority (as
opposed to something in the imagination of a programmer doing premature
optimization), then languages like C and Rust have a role. But you pay a
coding-time price for their use and I don't think that will ever go away.
I am a professional C++ programmer, and I now use Rust not only as a C++ replacement in my side projects, but also in places that originally belong to scripting languages. While it is good to see Rust attracts a wider audience, I hope Rust will not make compromises to the current design philosophy in an attempt to attract and retain wider audience when facing trade-offs.
Rust is a systems programming language that puts safety at the first place. Among others it has the same zero-cost abstraction philosophy as C++, that is, as Bjarne Stroustrup puts, you don't pay for what you don't use.
It favors explicitness and correctness over the ability to write one-off code fast when these two are in conflicts, because being able to write correct code that is easy to read and maintain is more important than being able to type less characters for the type of programs Rust is aiming for.
Rust also makes strong recommendation on what is the right way to write programs, but does not dictate it. This is reflected in that Rust design explicitly makes it easy to write correct code, hard but not impossible to write potentially bad code.
These made choices in design in my opinion make Rust to be the best in systems programming. And I hope it will always be the best systems programming language, but not just a good language for everything.
Please I wrote a respectful post. No need to introduce dubious (and frankly condescending and bit arrogant tone) ad hominen assumptions. I've coded in many languages also over a roughly 30 year career. Haskell does invert the type system from inductive to coinductive and this introduces advantages and disadvantages. I won't go into all the details further here (a link was already provided to Robert Harper's blog post). I was merely trying to point out that reasoning about Haskell down to the "nuts and bolts" requires understanding many other layers such as Monads (category theory), memoization, lazy evaluation, coinductive types, etc.. It is a high-level in the sense that is a very powerful semantics built with powerful abstractions.
That was my point also.
Not only that. There is also a tradeoff in the power of the abstractions we choose and whether they are comprehensible to other people that need to read and work on the code. Also whether those abstractions are the ideal fit for the task and use case.
This formerly absolutely valid point is weakened or mitigated as mobile eats the desktop and battery life also depends on performance. Nevertheless the cost of the programming effort remains a variable in the equation, so there is a balance which varies for different use cases.
This is why I argue the low-level languages should be used after profiling the code and knowing where the performance bottlenecks are. Because for example modeling memory allocation (lifetimes and scopes) in the type system, infects the type system every where (e.g. even 'a
in generics apparently) so the code base presumably gets more unmanageable over time because there is an exponential explosion of invariants. Because unsafe
can propagate from any untyped code (the entropy is unbounded due to be a Turing-complete machine, i.e. not 100% dependently typed). Thus the notion of a completely typed program is a foolish goal. It is about tradeoffs and fencing off areas of maximum concern. The human mind still needs to be involved.
So please don't try to hold Haskell up as the a high-level solution with the only tradeoff being performance. The reality isn't that simple.
Edit: for example (just so you don't think I am BSing), Haskell can't offer first class disjunctions without forsaking its global inference that provides some of Haskell's elegance. Without first-class disjunctions (which Rust also doesn't have), then composition via ad hoc polymorphism is somewhat crippled. I suspect some of the need for higher-kind types could be avoided if Rust had first-class unions. Which Rust could I presume implement, because it doesn't have global inference.
Yes but my point is that Bottom in Rust is not at the top of all types, i.e. Bottom (the conjunction of all types) is at the bottom of the type hierachy in Rust; whereas, in Haskell it is at the top of the hierarchy. At the Top of the hierarchy in Rust is afaik Any
which is the disjunction of all types, but this at the bottom of the type hierarchy in Haskell. This inversion of the type hierarchy in Haskell to coinductive has advantages and disadvantages.
Moderator note: All, please keep the conversation constructive.
My subjective opinion follows. Full respect for the person I am replying to is intended.
One can also avoid segfaults by using GC every where and then avoid all that tsuris of typing the memory lifetimes and scopes. So what you are really implicitly claiming is you need performance every where. But do you really? And what are you forsaking in productivity by not using a language with less tsuris?
I'll posit that programmers need productivity more than performance 80+% of the time. The amount of code that needs to be optimized in an application for performance is usually the smaller portion.
If we shift the focus to ad hoc polymorphism, then I'll agree with you that we need this 80+% of the time so we can express the semantics of our program, which can increase our productivity through:
- S-modularity (separation-of-concerns, SOLID principles)
- Less bugs
- Self-documenting code, code that is easier for others to learn and easier for the author to revisit
- Greater decentralized collaboration because of #1 - #3, which fits well with the DVCS open source, virtual work model.
This is why I am prioritizing the ad hoc typing over the typing of memory deallocation invariants. I'd still like to have the latter, but I certainly would not have made it the default at the cost of making the GC case fugly noisy. Why make the default the more noisy and less often used caset, which then makes very verbose the more often used case that could have been noiseless.
All portions of systems programming code is fully optimized? So Rust is for programming operating systems, web browsers, and FinTech (banks)? I bet even those can benefit from high-level code that is prioritized on expressiveness, concise readability, and other measures of productivity and human maintainability, other than memory allocation performance optimization every where.
It is not zero-cost. There is a cost in terms of human factors and also potentially as I wrote:
I am very skeptical of the claim that memory lifetime and scopes typing everywhere is the right and correct way to program.
I am very skeptical of the claim that memory lifetime and scopes typing everywhere is the right and correct way to program.
Given that the other options are manual memory management (without compiler support) and using a GC (and still suffering from data races/iterator invalidation). I don't share your scepticism.
To the extent the bold portion is predominant and we ignore the italized "everywhere", then I agree with you. That is why I wrote "skeptical" and not "certain" nor "confidenced".
However, I am roughly certain that it is trading one problem for another problem, in that the entropy of programming is unbounded and thus we can't type the universe around us (the I/O, the openness to other modules, plugins, FFI, DSLs, etc). So we'll end up with data races and other errors with Rust also. And if we try to type the memory lifetimes everywhere, we will end up with much more brittle type system. If we rather limit its exposure perhaps we can keep the explosion of invariants sane.
So again my skepticism is about wanting to use Rust's compile-time memory lifetimes model everywhere, not about the value of using memory lifetime and scopes typing somewhere. I am talking about balancing priorities, not about absolutes.
My subjective opinion of course.
P.S. I am the guy that wrote in 2011 that Rust was wasting their time implementing Typestate because it proposed to have two orthogonal means of enforcing invariants thus would create corner cases of a non-unified typing system. Typestate was removed from Rust.
Edit: my lesson learned from Scala was all that complex typing and corner cases which has caused companies to abandon Scala had also ignored one basic fundamental flaw, which that subclassing is an anti-pattern. Complexity kills. Often a high-level perspective can route around it. K.I.S.S. principle.
Edit#2: data races can also be attacked with co-routines and/or async/await with a single-threaded model, which adds functionality not really tsuris.
Edit#3: iterator invalidation can also be attacked with functional programming and Functor map.
You've said elsewhere that you haven't done that much Rust yet; experienced Rust users don't report a ton of difficulty with this. There's a learning period, and then it's not hard anymore.
Also, you end up using lifetimes a lot less often than you'd think. For example, over the weekend, I built a little text adventure for Ludum Dare. It's about 350 LOC, and loads a game from a TOML file and plays it. Not a ton of features, but still: I wrote exactly zero 'a
s in the entire thing. And I only have one place where I did some cloning that I may not have to. (source: https://github.com/steveklabnik/ludum/tree/master/src )
I appreciate your thoughtful reply and patience with my lack of experience with Rust.
I'd also like to read about the use cases which I can only do best with Rust's memory ownership model, which can't also be solved with:
- GC to remove segfaults
- Single-threaded asynchronous programming to remove race conditions
- Functional programming such as Functor map to eliminate iterators (and their invalidation)
Obviously where we need more performance, #1 is not acceptable. Like to see other cases enumerated here or in some document. Not a demand, just stating what I'd like to learn.
Any resource which isn't memory. Files, sockets, anything like that. GC only really helps with memory, but not anything else.
@shelby3 First, I want to reinforce @steveklabnik's point here: I [wrote]( Rust code for three months before having to write (let alone think about) a single lifetime annotation. I knew they were there, but I was productive without them anyway. Lifetime elision is awesome.
Second, I really think that Rust's model is superior to both pure functional languages (because sometimes mutation is darn useful) and GC (even if acceptable for performance), because once you have internalized the rules, code locality is pretty damn high – very seldom do I need to look outside a function to know what it does.
Also note that iterator invalidation is just one symptom of the problem of mutability + aliasing. So even with functors, you still cannot be sure that other code you are calling messes with your data. Ok, you can make everything immutable and rely on GC, but that won't help you if one unsafePerformIO
you didn't know of breaks everything.
You are again demonstrating what I am guessing is a lack of real
experience with Haskell, and therefore you don't know the difference
between theoryand practice. I have written a lot of code in all the
languages you mention(and many you haven't -- I've been doing this for a
very long time) andnone of the theory you spout above was remotely a
consideration. It's for academicians to debate and write papers about, but
it doesn't come up inthe real world.Please I wrote a respectful post. No need to introduce dubious (and
frankly condescending and bit arrogant tone) ad hominen assumptions. I've
coded in many languages also over a roughly 30 year career. Haskell does
invert the type system from inductive to coinductive and this introduces
advantages and disadvantages. I won't go into all the details further here
(a link was already provided to Robert Harper's blog post). I was merely
trying to point out that reasoning about Haskell down to the "nuts and
bolts" requires understanding many other layers such as Monads (category
theory), memoization, lazy evaluation, coinductive types, etc.. It is a
high-level in the sense that is a very powerful semantics built with
powerful abstractions.
I made the "guess" about your Haskell experience (not an assertion) because
you considerably overstated, in my opinion, what a programmer needs to know
in order to use Haskell to build real-world applications. And you continue
to do so above. Programmers don't "reason" about languages and programs.
They don't generate correctness proofs. I would venture another guess that
if you used the word "coinductive" to the vast majority of them, the
response would be "huh?". They write code, test it, release it, etc. They
do it by learning the sub-set of the language they need to get their job
done. This is precisely what I have done with Haskell, and it has been
enormously useful to me.
Because of the power of today's hardware, we can do a lot of useful things
even with interpreted languages, and with compiled languages that deliver
less-than-maximal performance, like Haskell and Scheme.This formerly absolutely valid point is weakened or mitigated as mobile
eats the desktop and battery life also depends on performance. Nevertheless
the cost of the programming effort remains a variable in the equation, so
there is a balance which varies for different use cases.
I agree. That doesn't change the main point at all, which is to use the
right tool for job and by "right", I mean minimize development and
maintenance cost. Driving a Ferrari to the supermarket doesn't make a lot
of sense.
But you pay a coding-time price for their use and I don't think that will
ever go away.This is why I argue the low-level languages should be used after profiling
the code and knowing where the performance bottlenecks are.
I couldn't agree more. A good deal of the early part of my career was based
on not fixing things until you knew for sure how they were broke.
Because for example modeling memory allocation (lifetimes and scopes) in
the type system, infects the type system every where (e.g. even 'a in
generics apparently) so the code base presumably gets more unmanageable
over time because there is an exponential explosion of invariants. Because
unsafe can propagate from any untyped code (the entropy is unbounded due
to be a Turing-complete machine, i.e. not 100% dependently typed). Thus the
notion of a completely typed program is a foolish goal. It is about
tradeoffs and fencing off areas of maximum concern. The human mind still
needs to be involved.So please don't try to hold Haskell up as the a high-level solution with
the only tradeoff being performance. The reality isn't that simple.
Read carefully please. I never said that coding ease vs. performance was
THE tradeoff. It is A tradeoff. There are certainly other considerations
that good programmers must make when choosing their tools for a particular
job.
Edit: for example (just so you don't think I am BSing), Haskell can't
offer first class disjunctions without forsaking its global inference that
provides some of Haskell's elegance. Without first-class disjunctions
(which Rust also doesn't have), then composition via ad hoc polymorphism is
some what crippled. I suspect some of the need for higher-kind types could
be avoided if Rust had first-class unions. Which Rust could I presume
implement, because it doesn't have global inference.
I don't doubt at all that what you say above is true. What I doubt is its
immediate relevance to the real world. We have examples galore of people
doing wonders with very flawed languages, e.g., C, C++, Tcl, Javascript.
Haskell and, I'm sure, Rust, would be ranked far higher by programming
language connoisseurs than any of the examples I gave. And yet those
example languages have been used to create an enormous amount of useful
software. Should we use the kind of theoretical considerations that you
seem to revel in in the design of future languages? Of course. But I don't
think it terribly relevant to making a programming-environment choice in
the world as it is today.
And thats why I expected the most frequent case of borrowing would be implicit so I don't have to annotate my code until I need it, but it isn't because of some mostly subjective choice to be more explicit by default. I thought the slogan was "zero cost" abstractions.
Edit: especially I shouldn't be forced to annotate pure functions. Obviously they can safely borrow.
I've read that fully optimized immutable functional programming can only get to within a log factor performance of imperative mutable programming. And as you allude, sometimes it is much more difficult to structure the same algorithm in an immutable way.
I think it is often more useful to think in higher-level terms. Depending on what the use case and design is, there may be very elegant ways to provide the necessary guarantees rather than falling back to a finely grained bottom-up approach to a memory ownership model. The fine grained approach intertwines a lot of minute invariant declarations and inferrants. Sometimes there may be a higher level design solution. That is why I am saying that Rust's memory ownership model everywhere is not likely to be optimum. It will hopefully have some very well matched use cases.
That phrase specifically refers to runtime cost. There is always some kind of cost, somewhere.
Saying that "explicit is better than implicit" is a shorthand for a more nuanced situation. The borrowck is a fully integrated part of Rust's type system, &
is an operator which transforms a type T
in a scope 'a
to a type &'a T
. A language could perform this operation implicitly in certain expression positions, but this results in a real hairy mess in my opinion. I definitely prefer telling the compiler when I want to perform this operation to the compiler applying some arcane heuristic to perform it for me.
My impression is that proper whole program lifetime inference is undecidable in a turing complete language, but I don't know if that's ever been proved.
Speaking more generally, you seem very interested in the big picture language design questions that Rust has encountered and grappled with. These are interesting and exciting questions! I would just encourage you to learn and use Rust more thoroughly before coming to conclusions about the effectiveness or viability of its approach in different contexts.
Afaics, the linked discussion seemed to indicate there wasn't a potential hairy mess, but I suggest our detailed discussion if any (and I'm not pushing for it as I think the decision has already been made) should go in that linked thread, not here.
Without any implicit conversions at all, languages become a hairy mess of noise. So to argue against one set of implicit conversions (while retaining others) by implicitly claiming you don't want the compiler to do any implicit conversions is really an arbitrary thus vacuous reason. Rather we need to either show ambiguities it creates and discuss the tradeoffs of the (what is actually mostly a subjective) choice.
I expect that to be easy to prove.