Rust as a High Level Language

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.

1 Like

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 'as 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 )

2 Likes

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:

  1. GC to remove segfaults
  2. Single-threaded asynchronous programming to remove race conditions
  3. 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.

3 Likes

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. :wink:

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.

2 Likes

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.

But those can often be handled with a simple destructor, because their ownership is rarely shared through the same handle.

An interesting post at LtU today that is probably relevant to this thread:

C is Manly, Python is for “n00bs”: How False Stereotypes Turn Into Technical “Truths”

I will take a break from this forum for a while. Thanks to all for the discussion and helpful replies.

How awful, that article.

I mean, I am not interested in Rust because I am manly..
Python is excellent for exploratory programming - I have used it for that myself.
Lots of highly talented people are using that language.

However, my 'calling' is graphics programming, so I am a C++ programmer (and not a very good one) - Rust seems to be a Godsend: the best of all worlds.
I need full control - to the metal - and if I can get that and have high level language features at the same time: win! :smile:
Also, I need smooth interop with C, and Rust have that.
And it does not require a massive runtime - Python is not really easily deployable..

I will probably still use Python for certain things, although - coming from a C and C++ background - I would probably try and use Rust more in those Python'esque situations..

1 Like

Given the programmer of a Haskell clone that compiles to Javascript—which Haskell donated two of its Google Summer of Code slots to—documents that the Num(ber) class is constructed from the typeclasses (aka traits) Semiring, Ring, and Moduloring, I maintain that the target demographic for Haskell are the more mathematically minded.

One can produce programs without knowing how the libraries for a language are designed, but to be an expert one typically needs to understand the "nuts and bolts" of the language and the libraries.

Edit: for example, understanding one way to do coroutines in Haskell.

Edit#2: An explanation of the Applicative functor for Haskell, goes into how side-effects aren't allowed for pure functions, yet we can model side-effects in the higher-level abstraction of the Applicative functor. This has the point of being a counter-example for donallen and also pointing out that it is possible to model mutability in immutable constructions by lifting the abstraction (and there are no memory leaks within the pure context, although we can create memory leaks when we model mutability yet our abstraction isolates and types the mutability so other potential means of automated static analysis could be envisioned).

In my experience, even with a garbage collector, developers aren't completely free from memory management. Garbage collectors definitely help make sure memory accessed is always valid. However, I can not tell you how many times I have had to debug memory leaks because other developers don't consider what holds onto a reference and how long that reference will live. I can honestly say I have become a better Android/Java developer because Rust has been forcing me to think about these things and that even with a GC they can not be taken for granted.

2 Likes

I liked your post because it is a good counter-argument. I want to point out though that I had pointed to higher-level paradigm shifts to attack the problem of memory leaks (which in some localities entirely eliminate thinking about memory management in some cases, e.g. pure functions), other than Rust's fine-grained (low-level) approach. So I'll continue to argue that Rust's approach is not going to be employed everywhere.

I've taken a hard look at Rust and I don't think it's appropriate for
situations where ultimate performance isn't necessary. Systems that provide
garbage collection are much easier to use. In Rust, you are doing memory
management manually, as you do in C, but Rust insures that you do it
correctly, which C does not. Garbage collection removes memory management
from the programmer's set of responsibilities.

In my experience, even with a garbage collector, developers aren't
completely free from memory management. Garbage collectors definitely help
make sure memory accessed is always valid. However, I can not tell you how
many times I have had to debug memory leaks because other developers don't
consider what holds onto a reference and how long that reference will live.
I can honestly say I have become a better Android/Java developer because
Rust has been forcing me to think about these things and that even with a
GC they can not be taken for granted.

I was talking about the lesser load garbage-collected languages place on
the programmer, compared with languages like C and Rust, where the
allocation and deallocation of memory is the programmer's responsibility. I
did not intend to make the claim, though I can understand why you thought I
did (because of the way I wrote the last sentence above; what I meant by
the phrase "memory management" was explicit allocation and deallocation)
that there is no way to write code that uses far too much memory in systems
equipped with GCs. I have written a lot of Lisp and Scheme over a period of
more than 40 years and have had to pay attention to exactly the issue you
raise (especially 40 years ago, when memory was a much more scarce resource
than it is today) -- retaining references unnecessarily; this is why many
Lisp implementations provide "weak" references. Unnecessary consing is a
similar consideration, as is to take advantage of tail-recursion when
possible.

[Moderator note: Fixed some posts with broken Markdown formatting, and removed two comments discussing the formatting issue. Please feel free to flag posts (choose the "Something Else" option) with mangled formatting, or PM the author. Users replying by email, please make sure quoted text is followed by a blank line.]

3 Likes

Eager coercions are a very hackish feature. They interact terribly with inference, causing order-dependency and other craziness. If you add too much of them, type inference becomes so loosely-coupled it tears itself apart (see functions-destroy-closure-inference).

Rust's original approach was to use Rc (then @) everywhere so that nobody had to think about memory management. It's just that we found Rust's ownership/borrowing semantics to be sufficiently nice we started using them everywhere, through @ still exists.