-
Notifications
You must be signed in to change notification settings - Fork 7
Optionally declare functions pure
?
#4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I would rather have monadic types for impurity. In this way we have a sound type system that can be expanded for full effects support. I would like to have inferred monadic side-effects. The inference means the average user does not need to bother or understand them, but you can use type classes to constrain the types to achieve things like this function must not do IO, or must not allocate memory etc. If all the primitive operations have the correct mondaic types assigned to them, purity becomes constraining a type to have no side effects. I haven't really thought about the effect syntax, nor how a constraint on effects would look syntactically, but this achieves what you want with more fine-grained control about which side-effects to permit. |
If operations are correctly typed and maybe with some syntactic spitshine, we can make monadic types look very simple, I'm all for just going with monadic types. We can probably make it so the user doesn't even realize he's using monadic types. |
Lets assume we have monadic types, and that all functions are in some monad (as we want to allow imperative style statements anywhere). Further lets assume we have a monadic effect system, with effect inference. As such effects will propagate from the function use site to the definition (the effects on a function being the union of all effects in functions used in the function definition). As long as primitive functions get the correct types, it should all work without the user even knowing. They only need to know if they wish to constrain effects, probably with some kind of type class. In order to do this the type system will need to provide some a 'set' kind of type, but I think we need that for union types already. Having to import functions from JavaScript and give them a type seems like some boilerplate, but I don't think there is any other way. For example a simple primitive 'print' type function would have a signature like:
A primitive input function would type like this:
Now you can use them like this:
The type of doit can be inferred, however you can give optional type annotation like this: So the assumption is all functions are in the effect monad, and therefore we can omit the effect constructor '[]' from the type signatures of functions. |
I like that. A lot. |
My understanding of a monad in this context it provides composition of functions that mutate some state (i.e. input and output the state type) with those that don't. The advantage over compiler-assisted access to globals, is we can have different state objects (e.g. different implementation of But separately we would still need a way to declare a function's inputs arguments and constructed references (collectively or individually) deeply immutable (which is not the same as JavaScript's |
Technically in an imperative language where any statement can perform IO to any external service, every function is in a monad :-) The threaded state is the 'world' and |
We need purity declarations, otherwise we model values separately from functions, which is a non-unified construction. Note that the purity declaration for an immutable reference and a zero argument function can be unified to |
Then we get an arbitrary split, some functions are pure and some functions are impure. I can only call pure functions from pure functions but I can call either from impure functions. This is how Haskell works and it as a real problem for a lot of people to work with. You want to put a print statement in a pure function somewhere and all of a sudden you have to rewrite half the program. If you want an easy to use language that Java programmers will be able to use, we cannot do this. This would make the language feel more like Haskell than any of the type system features I want. I think we want all functions to be impure and in the IO monad. We can then use constraints to prevent side effects where we don't want them, but the functions will still be impure. The separation between function reference and result value is very important. |
@keean wrote:
I thought you were planning to offer treading global state through pure functions with an implicit/inferred monadic system. So Seems to me we can't do your monadic idea unless we do it every where. But am I correct it will only add overhead to calls to functions which access the monadic state or will the performance of every function call suffer?
If we are forced to have your monadic system every where, then why should all functions be impure and in the IO monad by default?
I don't understand what you do you mean? |
If we will have pure annotations on functions, then we also need |
@keean seems to me silly that for example a |
There is zero implementation overhead, it is simply a type system change. I have pointed out before you can give 'C' functions a monadic type, do the type checking and then compile it using the normal C compilation method. |
Because we may want to change the function to include IO without having to change all the module type signatures.
A program is a bunch of symbols that is executed to produce a result. It is important we can pass around the unexecuted function and then execute when we want. I think it is important that the notation is consistent no matter how many arguments a function has. Making an exception for zero argument functions is introducing special case inconsistent notation.
Debuggers should not make up for inadequacies of the language. Programs are rarely written in their final form and the ability to easily refactor is important. |
@keean wrote:
I don't understand. What do modules have to do with it? I probably need to see a code example.
Afaics, syntax of zero argument functions is orthogonal to a purity annotation decision and discussion. @skaller and I enumerated optimizations and benefits of purity. For example also the following also applies to non-zero argument pure functions:
What inadequacy? Purity annotation is an optional feature. |
Readers please note that certain side-effects are not side-effects in our type system. More discussion about the relationship to IO monad.
I wrote:
|
@skaller and I already explained that. I also explained that even a non-zero argument pure function can't be invoked without assigned the result value, so the compiler can catch this bug:
|
@shelby3 okay well I think a zero argument function should be executed... it is not the same things as a value. Scala got this wrong :-) |
@keean there is no reason to execute a pure zero argument function. That is nonsense. And there is no reason to execute any arity pure function if the result value is not assigned. |
@shelby3 It may fail due to lack of memory. A value is a value like '4' or '7'. A function has a value which is the set of symbols for example "4 + 7". Before you execute it is is "4 + 7" when you execute it, it returns "11". If it is a const function the compiler may optimise by executing it at compile-time. (By the way @skaller was saying the same thing). To me it seems you want to save typing two characters |
refresh again. |
We can analyse this further. The only time a pure function is really pure is when all the inputs are known at compile time (in which case you can treat it as a const). If any of the inputs to the function depend on IO then the pure function has to be lifted into the IO monad. To put it another way, when we treat a function as pure and replace it with its value it means the input cannot change. If the inputs to a function change, its results will change to. You can see this in Haskell:
If we don't do this the program will go wrong, because the compiler will not know that the result of 'f' can change with time, and it will replace it with a single value that won't change even if we run |
Of course if a function has no side effects, and no arguments, it must be a const value (IE the compiler can replace the function with its value at compile time). If this is the case you can declare it as a value instead, just do:
It seems there is no need to ever have a pure function with no arguments. The only useful zero argument functions have side-effects :-) |
@keean don't forget that (which I pointed out before) a function which takes a callback of a zero argument function, would also accept a pure such function. And this is another reason to treat const values and pure zero argument functions as equivalent and also pass them as callback for (pure and impure) zero argument functions. |
@keean wrote:
...
I already linked to were @skaller and I refuted that entire line of reasoning. I am not going to repeat it.
You are conflating two orthogonal concepts. The value of the pure function never changes and no side-effects occur (that are modeled in the type system). The inputs select which element of the (lazily memoized) set of the function's value is presented. An impure function isn't guaranteed to have the same result for any fixed input, i.e. the set of results that is the function's value is not constant.
No I want a system that is complete, consistent, and not corner cases. It appears to me sometimes you seem to want consistency by not being complete, which is a form of obscured inconsistency. |
To make progress I suggest implementing basic stuff, when in doubt leave it out until there are actual uses cases to consider. So, do not put "pure" yet. Do not have => for functions and -> for functions with IO monad, yet. These forums are not the best for technical discussions IMHO. For a start, you can't reply to someone as you can with email. We need an email list which tracks commits. I can write some test cases, but i would need commit access and a fairly clear specification of what is implemented and what it should do. As soon as possible, I think an FFI is required, and then a place to put a library of bindings to Javascript. Most of the work you can do with the language can then be done almost immediately by adding the required bindings and writing Javascript to bind to. No matter what you think the advantages of Zen-fu are the primary one is the ability to leverage existing code, but access it in a better structured way (eg with type checking). One thing I learned on the C++ committee is that the library drives the language. You add stuff you need to make the library work. So for example Felix has a restricted kind of constrained polymorphism where you can define a type set (a finite list of types), and constrain a type variable to that list. Why? Because that's whats needed to define addition of all 20 types of integers C++/Posix require in one line. (Just an example). |
@keean you didn't really address my question directly. I am agreeing with you that the IO monad is not composing functions that are impure w.r.t. to the world because the access to the world is sequenced by bind, but those functions must also otherwise be pure, i.e. they can't be allowed to save a mutable reference to the world because then they could violate the control of ordering dictated by bind. The access of mutating the world is really not pure, but it becomes pure because the mutations are ordered by the bind. Thus I am saying you were wrong to imply that Haskell allows impure functions or that IO monad will work correctly with impure functions. The functions are actually impure w.r.t. to their mutation of the world, but because they are not otherwise impure (i.e. they can't save a mutable reference to the world) and because their access to the world is ordered via bind, then in effect they are pure w.r.t. to the world. |
Given a completely pure program like (a + b + c + d) the compiler is completely free to execute (a + b) and (c + d) in parallel, but must wait for both of these to finish before it can do the final add. However it needs to choose the correct order of additions to get the optimal parallel execution time. However the context switch time (for example the standard unix context switch is 100 times a second) means that very small tasks are inefficient, so this kind of parallelism is actually slower, due to the overhead of context switching and the quantisation of CPU time. On a Unix system for example optimal parallelism is when tasks take of the order of 10 milliseconds or longer. |
I don't see how your comment regarding the cost of context switches is at all relevant to my point. The fact that a particular case of higher-level parallelism is not worth executing in parallel is irrelevant and has nothing to do with my point. The point is that higher-level parallelism has access to invariants that low-level CPU level parallelism doesn't have. (And yes the compiler along with any profiling runtime optimiser a la Hotspot could determine which instances of higher-level parallelism are not cost-effective to execute in parallel but that is besides my point) |
The trick is in the monad itself, and is more to do with object naming than anything else, consider:
In the above the world is immutable, but we can "input" from it returning a new "world" hence we model IO sequencing in a pure functional program. However we have to keep track of the different versions of the world, and we can re-use earlier worlds (which would give us back-tracking in a parser). This is threading the state through a pure computation. If we look at an OO program with state we might right:
So the difference between the above functional version, and the OO version is that in the OO version the 'world' refers to the all versions together. Hence mutation can be seen as giving the same name to the object before and after it changes, whereas a pure function must give a new name to the new version of the object. A monad lets you make the former (pure program) look like the latter (impure program) by hiding the state threading, but unlike in the OO sense the threading is still happening, so all the code is actually pure, although it behaves like impure code so:
The monad is handling the threading of the world implicitly, and this is why you can have things like "MonadPlus" which is the equivalent of saving a reference to the world and reusing, hence giving backtracking. So if the real trick to the monad is that it allows pure functional code to behave like impure imperative code, the final amazing thing is that you can view a monad as a pure type-system transformation, so if we take the 'C' program:
We can type 'getLine' as a monadic function "IO String" without changing the 'C' code. We can type check the 'C' code as if it were a pure functional program. |
In other words, as I wrote, the functions Note the world is not actually immutable because you can't copy the world, so you can only mutate it. But we can pretend that our function Which is equivalent to what I already wrote. Our brains work differently. I like to distill to the generative conceptual (abstract) essence. You seem to prefer to think in terms of structures (i.e. algebras). Your essence is mathematical. My essence is conceptual. This why you tend to explain with notation and find it difficult explain well in words. |
Okay now that we've clarified monads, I feel more confident to conclude that your proposal for having tuples of effects will not compose with monads. The entire reason that monads don't compose well, is because they must maintain the ordering on the particular lifted type. If you mix and match lifted types, this ordering can't be maintained in the permutations of composition and mis-matches. Monads enforce a total ordering on access. This is why they don't generally compose. |
Actually composition of monads is quite easy providing you have the right mechanisms. If you start with a type-indexed-set, then each monads state can be indexed by a type. We are effectively unioning monads together, where each monad can select its state from the type-set by a unique type. If you do what Haskell does with monad transformers you end up in the "lifting-mess" that you describe, but with effect sets there is no lifting, beyond the simple one-level lifting of in the monad or not. This is the doing the equivalent for monads, that union types do for simple types. |
I don't understand. I have stated that lifting unions of types is incompatible with bind under composition of different unions. Bind can only work with the same lifted type. Thus your unions will have to match. Thus composition becomes impossible when they don't match. It seems you want unions of types as indication of which effects are allowed. Okay fine. But it doesn't compose well with bind. In other words, the logic on supersets doesn't compose with bind, i.e. you can have a match that is permissioned but won't compose. |
You lift both sides of the bind, so the monad constructor is the union of all monad constructors used in the function. Note the union occurs in the constructor, not the parameter, so the union of a monad |
I wrote:
|
My rambling
See also the Mutable Objects #32 |
Algebraic EffectsI’m making a reasonably detailed exposition targeted towards even those who aren’t well versed in Haskell or category theory, both because my desire is to produce a mainstream programming language (so I might as well start now trying to layout these concepts in a manner that non-researchers can grok) and for myself to refer back to if in the future I’ve forgotten some details. Picking up again my naive investigation and steep autodidact learning curve from 2011 (c.f. also here and here), recently I’ve been (along with some assistance from @keean in private messages) analyzing algebraic effects as possibly a better model for controlling imperatively ordered5 effects. Such effects include I/O, exception handling, callbacks, and access to state without referential transparency. Applicative algebraic effects commute; and thus modularly compose unlike for monads[*] which require transformers that discourage modular composition. I was exposed to algebraic effects in my search/research for how to achieve goroutines in Multicore OCaml, and then @keean mentioned to me Levy’s CBPV (call-by-push-value as unification of call-by-name/CBN and call-by-value/CBV). What motivated me is realizing that the value restriction can only be avoided with applicative (aka pure) programming (i.e. the restriction exists to prevent purity aka referentially transparent assumptions about parametricity that don’t hold in an impure context, c.f. also §Separation of values & computations on pg. 3 of An Introduction to Algebraic Effects and Handlers1), and that the value restriction is indicative of lack of equational reasoning (i.e. as evident by the restriction preventing application of functions to functions as a parametrised value, which is Scala’s syntactical restriction). And also the realization that modularity requires applicative ML-like functors. IOW, continuing to program in a C-like or JavaScript-like imperative style isn’t going to produce modular, referentially transparent effects which can increase flexibility of code reuse and decrease bugs. So perhaps we need a new paradigm. And hopefully one not as abstruse as Haskell and without the often unnecessary lazy evaluation (aka call-by-need) which complicates reasoning about implicit space and time effects. Backtracking example employing algebraic effectsThe example for backtracking with algebraic effects in §2.3.2 Backtracking on pg. 8 of An Introduction to Algebraic Effects and Handlers1 2:
A key point in understanding the above backtracking example is an inversion-of-control such that the This coinduction is more explicit in the §2.3.1 Maximal result on pg. 7, because the
Note For the backtracking example, the depth-first algorithm is by applying Complete understanding of operational and denotational semantics requires learning that the The subsequent Fined-grain monadic continuations section is a primer on the relevant domain knowledge that applies to understanding the prior paragraph. Translated to pure functional and imperativeBelow are the example code translations for the equivalent operation semantics interpretation employing pure functional programming and imperative equivalents. The
Modular generic flexibilityAs exemplified for the Fined-grain monadic continuationsPer §1 Language on pg. 2 of An Introduction to Algebraic Effects and Handlers, P. Levy’s fine-grain call-by-value is the programming language concept employed in the algebraic effects examples of this post.
Thus, the The The bind operation can be seen in the example from §2.1.2 Reversed output on pg. 5:
Per Sequencing in §1 Language on pg. 2, the semicolon is a syntax sugar for the
Above per Language extensions in §1 Language on pg. 3 the The
Written in a more JavaScript-like syntax pseudocode:
The laws which every monad must obey insure that regardless whether our programming language is CBV or CBN, the P.S. For more information about shallow handlers from §2.1.1 Constant input on pg. 5 (and more in depth information on other facets), c.f. §2.6 Handling by Application on pg. 4 of Do Be Do Be Do. 1 Paper was also mentioned in §3.6 A tutorial on algebraic effects and handlers on pg. 6 of From Theory to Practice of Algebraic Effects and Handlers by Andrej Bauer et al. 2 Incorporating my proposed 3 Filinski, Declarative Continuations and Categorical Duality, §1.3.1 Basic definitions, §2.2.1 Products and coproducts, §2.2.2 Terminal and initial objects, §2.5.2 CBV with lazy products, §2.5.3 CBN with eager coproducts, §2.5.4 A comparison of CBV and CBN, and §3.6.1 CBV and CBN in the SCL. 4 The example is an anonymous function 5 To understand how CBN can have computation in value types, note that an object and its properties can be modeled with continuations. And imperativity as an orthogonal concept to referential transparency (aka pure functional) can be expressed by the (partial and/or compositional) application order of computations/functions combined with altered copies of said objects which how a Monad models state. |
Functional reactive programming to avoid the imperative modality of the “spaghetti of listeners” doom, e.g. a practical GUI programming example use case, can be implemented with algebraic effects. I explained algebraic effects in my prior post above. EDIT: and a follow-up to relate it all back to algebraic effects and explain further the algebraic effects. EDIT#2: a more holistic explanation of algrebraic effects and it’s relation to the free monad. |
Regarding the discussion of I have referred to making copies of immutable data structures more efficiently, and the official term is persistent data structure. |
Referential transparency doesn’t mean there are no side effects. Referential transparency prevents the side-effect of referenced (through a pointer) shared state, but not state shared through some higher-ordered semantic such as monadic. @keean wants to declare which effects a function mutates such as: What is the advantage of doing so? |
@shelby3 the advantage is that you can then put constraints on effects like pure (no effects allowed), no heap allocation, no stack allocation, no arithmetic exceptions, which can then be enforced by the compiler. |
Yeah I got that part already, but I’m asking what practical advantage do such coarse grained constraints provide? I’m failing to visualize an example usage which is worthy. To avoid a kitchen sink design, features must be motivated by use cases. I noted you are (I think correctly) defining ‘pure’ as “no effects” instead of as just referentially transparent. |
How do you solve bottom-up propagation of effects? Do you allow to overwrite effects? |
@sighoya you can infer effects, so that keeps some of the boilerplate down. We can be row polymorphic on effects, IE.specify at least these effects allowed, and we can be negative about effects, everything except this allowed. I also think that effects that are contained should not escape. So an code that is impure can have a pure interface provided the impurity cannot escape. |
Do you mean that effects of subfunctions inside a function with effects are not propagated upwards? |
If they do not escape then yes. Consider a function that uses some internal state for example summing the numbers in a list. We might write:
Note, im experimenting with some syntax here '{0}' creates an object containing the value |
I think the whole function is pure. Local mutations in functions which do not escape don't make a function impure. What I mean are callbacks which cause effects:
|
I agree, and Haskell can use the State monad, and
The function that accepts the callback would specify which effects are acceptable. For example in an OS kernel setting an interrupt handler may prohibit memory allocation effects, so that we don't get an OOM in an interrupt handler, which is normally a hard unrecoverable crash (reboot). |
@sighoya wrote:
Just don’t do that anymore. |
@shelby3 wrote:
Purity provides more opportunities for optimization.
The text was updated successfully, but these errors were encountered: