So this has been bouncing around in the back of my head, and now actual
thoughts have congealed there. Essentially, while I think this is a very
good defense of why Rust doesn't have purity, it's not so convincing to me
as a defense of why it *shouldn't* have purity. (I don't know if it was
intended as one.)

On Tue, Apr 30, 2013 at 8:38 PM, Graydon Hoare <[email protected]> wrote:

> On 30/04/2013 10:08 AM, Max Cantor wrote:
>
>  I know this will be an unpopular opinion, but pure functions would be a
>> massive win for Rust, especially if the eventual goal is high
>> performance, highly parallelizable (browser rendering engines..)
>> development.
>>
>
> Careful. It's important to understand that "purity" seems like it has a
> simple definition but in languages with mutable memory, state, time and IO,
> it gets hard to be exact.
>
> Things you can throw at a SIMD unit or GPU and get parallel kernels out of
> them will almost certainly be a different version of "pure" than things you
> can evaluate at compile time. Or, as in our previous attempts at defining
> it, things that won't break existing borrows or existing typestates. Each
> of these is a static approximation of the
> set-of-all-things-a-function-might-do. Since our functions can generally do
> quite a lot, the set of possible subsets you might mean by "pure" is
> correspondingly much larger.
>
>
>  The typestate system did seme very complex but isn't there a middle
>> ground via annotations perhaps?  A subset of primitives and core
>> functions can be annotated as pure and then any function calling only
>> pure functions can itself be annotated as pure.
>>
>
> This gets difficult fast. You wind up dividing your functions up into
> groups and then getting annoyed that one that's "mostly almost pure" or
> "essentially pure, for my purposes" that you wanted to call actually isn't
> (someone forgot to mark it as such, or some sub-function, or some
> trait-implied function) and then you can't. Or is pure using one way of
> thinking about purity, but not another. Or is pure except for the bit where
> it calls unsafe but promises it's going to maintain purity, just knows
> better than you (oops, that can't be done at compile time, nor on a GPU,
> etc.)
>

Yeah, this is definitely a valid observation. Even Haskell is bitten by
this (if something always has the same value in a single run of the program
but may have different values in different runs, is it pure?).


>
> C++ has multiple concepts for this, each a not-entirely-obvious subset of
> the others, each affecting the others, and causing quite a lot of work just
> to get things to compile, much less reuse code.
>

C++ is C++. It has multiple concepts for *everything*. That something is
complicated in C++ does not imply that it has to be complicated by
necessity.


>
> They have const methods (don't mutate the object, unless you lie and
> override it) and constexpr (can be evaluated at compile time), and macros
> (can only operate on tokens), and template evaluation (can only operate on
> certain non-type args), and the openCL __kernel extension for
> GPU-applicable functions:
>

This is unfair even to C++. Const methods are the same thing in spirit as
`&self` versus `&mut self` in Rust. Macros in C++ and Rust alike are for
code generation, not computation. I wasn't familiar with __kernel, but I
looked into it[1] and found this: "The function using the __kernel
qualifier can only have return type void in the source code". If that has a
common intersection with any other definition of purity, it's `return ();`.

That leaves templates and constexpr. Which, indeed, have a lot of overlap,
and neither is a subset of the other (constexpr functions can't do types
while templates can't do values of user-defined types other than enums, for
example). Both are restricted to dealing in things that can be evaluated at
compile time.* The part of templates that deals in types corresponds to
Rust's generics, while the part of templates that can calculate the
Fibonacci numbers is a historical semi-accident. Constexpr expressions (and
hence functions) correspond to Rust's attempts to define a constant
expression subgrammar.

The only really legitimate notion of purity in C++ to my mind is constexpr.

[1]
http://www.khronos.org/registry/cl/sdk/1.0/docs/man/xhtml/restrictions.html

* Actually, constexpr functions *may* contain non-constexpr expressions,
but only as long as they are in the branch of a conditional which isn't
taken at compile time. Which is actually useful. C++ for you.


>  "Which purity do you mean" is a very real question, not one you can just
> brush aside. The combinations are worse, in that they tend to cause (or
> require) subtyping relationships, that touch _everything_ in the language,
> from inference and mandatory syntax (which types get inferred when you just
> write a lambda?) to type checking (which calls are legal, which impls and
> delegations are legal) to codegen (which LLVM attributes are legal? which
> things can we inline and how?)



 [...]



A long time ago we had an effect system and we made pure the default (since
> we didn't want people accidentally leaving it out due to sloth) and we made
> the impure specifier a very small and reasonable keyword: "io". It was
> still a heavy complexity bill (required a full extra dimension of
> subtyping, parametricity, etc.) and _still_ had people breaking the rule
> with `unsafe`, which meant that the putative benefits like "can do compile
> time evaluation" or "can spread on a GPU" weren't there anyways. And people
> couldn't do simple things like "put a printf in here for logging" (much
> like in haskell).
>
> Eventually people just took to making everything io, at which point it was
> a noise word and we decided to remove it (along with 'pred', which just
> meant pure, bool, and tracked by the typestate layer).


Again this is something I can definitely accept. Can't argue with history.
It would be nice to know for context how the old system worked. Is there
documentation anywhere?


>
>  Coming from the Haskell world, not having pure functions would be a
>> considerable deficit in the language that is so close to being a best of
>> both worlds descendant of C and Haskell.
>>
>
> The "direct descendant" aspect here is probably inaccurate. We're more of
> a descendant of ML with its mutable ref cells, eager evaluation and such.
> Haskell was willing to force its users to segregate all variants of
> impurity outside the most conservative intersection into monad
> representation[1].


FWIW it's not really a most conservative intersection. They can't run it on
the GPU or evaluate it at compile time either (except by compiling and
running it, which is what Template Haskell does). What they have is a
semantic notion of purity. If there's no way for pure code to observe
different values (or the user running the program different effects) at
different times, it's good to go, even if it requires unsafePerformIO to
implement. Using unsafePerformIO to cheat is frowned upon* and more
importantly isn't reliable.

* Except for Debug.Trace.trace[2], which has been called "a refreshing
desert in the oasis of referential transparency".

[2]
http://www.haskell.org/ghc/docs/latest/html/libraries/base/Debug-Trace.html#v:trace



> But this costs heavily in all code that doesn't fit that intersection,
> which is a lot of systems code; so we've decided against it. The split is
> too much for C programmers to accept; anywhere we try to draw the line
> appears to cause a lot of anger and resentment over mandatory type-system
> fighting.
>

Right. As above I can readily accept that it's not an easy question, that
the right design hasn't been found, and that earlier attempts haven't
worked out. What I find harder to accept is that there doesn't exist *any*
definition of purity that would be superior to nothing at all. C++ derives
some benefit from constexpr. Haskell derives a lot of benefit from enforced
purity. Surely there is some point in the design space which would be
beneficial for Rust. Perhaps a wiki page collecting the various options
with their applications, benefits, and drawbacks would be helpful?

(FWIW I sorta suspect the presence of mutability and lifetimes in the type
system might make an observable notion of purity a la Haskell easier to
define, but I haven't thought it all the way through.)


>
> -Graydon
>
>
> [1] This is why they could "implement STM in a weekend" -- by excluding
> almost all functions -- but I think this characterization is really unfair
> anyway. What they really have is just "do notation", which means
> constructing a suspended execution-tree is _slightly_ less miserable than a
> deeply nested tree of constructor calls and lambdas. But not really a lot.
> The interface-points with the rest of the language involve pervasive
> lifting, lowering, wrapping and unwrapping. See the "simple STM example" on
> their website: http://www.haskell.org/haskellwiki/Simple_STM_example


Are we looking at the same thing? The only lifting, lowering, wrapping and
unwrapping I see there is `atomically`. Any language with STM is going to
need some way to delineate transactions (ye olde atomic { }). Perhaps other
languages can get away with eliding it for single statements, but that's
not a tremendous amount of difference. I also don't see "excluding almost
all functions": it excludes all functions except for those which are pure
and those which only manipulate transactional data... in other words those
which do non-transactional I/O... in other words exactly the ones it has
to! What am I missing?

-- 
Your ship was destroyed in a monadic eruption.
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev

Reply via email to