Add pure functors to the module system - #13905
Conversation
…ractive of applicative context
|
Thanks @samsa1 for the description. However unless I missed something there is no specification of what a pure functor actually is :) I guess we are talking about functors whose body consists only of type definitions, function bindings and other pure constructs, not including possibly side-effecting operations (such as ordinary variable bindings |
|
Indeed I've forgot to add a specification. I added a paragraph to explain the intuition and actual restriction on what a pure functor is : mostly a syntactic restriction that rejects all possibly side-effecting operations |
|
There is a little-known compiler option |
| and functor_parameter = | ||
| | Unit | ||
| | Named of Ident.t option * module_type | ||
| | Named of bool * Ident.t option * module_type |
There was a problem hiding this comment.
This probably shouldn't be a boolean.
I think that this should either be a 'purity' type or this case should be turned into an inline record
| and module_parameter = Odoc_module.module_parameter = { | ||
| mp_name : string ; (** the name *) | ||
| mp_type : Types.module_type option ; (** the type *) | ||
| mp_type : (bool * Types.module_type) option ; (** the type *) |
There was a problem hiding this comment.
This should also be a purity type.
|
One thing that I think is not necessarily obvious, and is probably relevant to discussions of this feature, is that you can demand that only pure functors be treated as applicative without changing expressivity. That is because applicative behaviour only matters for types in the module, and given an impure applicative functor: module F (X : S) = struct
let foo = ...
type bar = ...
let baz = ...
endyou can always replace it with a pair of a functors: one obviously pure and the other generative: module F_types (X : S) = struct
type bar = ...
end
module F (X : S) () = struct
include F_types(X)
let foo = ...
let baz = ...
endThis ability to split an impure applicative functor is what makes them sound in the first place. That is why any feature which would prevent the split -- e.g. unpacking first-class modules -- is banned from the body of applicative functors. This means that -- from the perspective of expressivity -- you don't actually need to worry about how precise your tracking of purity is. If your purity checking has false negatives, then users can just do the transformation to get equivalent applicative behaviour anyway. |
This does not make sense to me and I would refer for this combination to be rejected. |
I agree that it does make much sense so it is never printed. The main reason I decided to make it legit was to have a simpler parser. However I'll look to see if it is possible to have a nice looking solution rejecting that case. edit : it seems like a 38 lines patch so it seems a good thing to go into that direction |
|
Maybe you could accept it in the parser and reject it in the type-checker ? This also makes it easier (for now) to produce a precise error message. |
|
Last week I had discussion about this feature with the flambda2 team (in particular @lthls, @chambart and @Ekdohibs). One of the points being how it could interact with the back-end and in particular while also considering modular implicits. One point that was raised was that if we wanted to be able to do CSE on modules expressions as presented in Implicit functors should be pure we also wanted to ensure termination (it is necessary if we want to be able to lift module expressions out of functions). However this corresponds to a stricter notion of purity than the one present in this PR that does not consider divergence as a side-effect. Side note : we should check that this PR behaves correctly with recursive functors as they might raise exceptions at application which is a side effect. |
I took a bit of time to look at the implementation of
I think that recursive functors behave the same way as non-recursive functors. In a recursive definition functors are not considered safe so they end up compiled the same way as non-recursive functors, except that they may refer to not-yet-initialised modules. But calling a function from another module (parameter or not) should never be considered pure anyway, so I don't think we need to worry about possible exceptions due to uninitialised modules. |
| check_well_formed_module env apply_loc | ||
| "the signature of this functor application" mty_appl; | ||
| if not is_pure then | ||
| check_purity apply_loc env funct_body (fun () -> false); |
There was a problem hiding this comment.
I took a bit of time to look at the implementation of
is_nonexpansivein this PR, and I couldn't find a way to create an expression treated as pure which diverges. In particular, function/functor applications are never considered pure as far as I can tell.
Yes, but applying a pure functor to an argument is currently considered pure. This might lead to a divergence coming from the module language and not the core language
|
I've been discussing pure functors (with termination) with @Drup today. He had new proposals to add to the mix :
However this seems impossible to do in practice after more thinking due to some examples that becomes difficult to detect such as : module M (G : (S -> S) -> (S -> S)) = G(F)
module M2 = M(F) |
For the record, this sets off alarm bells in my head. One of the nice things in OCaml is that it has well-defined semantics on most programs (maybe all, if you exclude primitives?), which makes it easier to reason about and debug, as put forward in the PLDI 2014 paper “Bounding Data Races in Space and Time”. |
In this PR we introduce a new type of functors : pure functors.
Such functors work like normal applicative functors and are coercible. The main objective is to be retro-compatible while
having the possibility to give stronger guarantees when writing a functor.
What is a pure functor
The objective of this definition is to make CSE valid on all such functors as explained in the motivation section.
A pure functor is an application functor with aditionnal restrictions:
is_nonexpansivethat tests purity of expressionWhat is allowed in a pure functor:
is_nonexpansive(already used for value restriction) that does not allowraise _,assert _and goes through sequences.Motivation
While working on #13649 I realized that not having a notion of pure functors in OCaml was highly restricting.
As such it is possible to do many impure operation inside a applicative functor such as:
Because of this we don't have the sharing of values between applicative functors and #13649 disallows all functor applications.
Having pure functors also allows new optimizations on module expression such as CSE which would be mandatory for something like modular implicits (Implicit functors should be pure).
However for backward compatibility reason we cannot force applicative functors to be pure (or at least not with a prior notice). As such I propose to introduce a new type of functors : pure applicative functors.
Behaviour
To be discussed
The first proposition would be to annotated pure functors using
=>(or the[@pure]attribute if people want to bevalid through multiple versions of OCaml) but it is open to discussion.
The syntax is what I think the most complex part of this PR. Which syntax do we want ?
The main constraints being : we don't have n-ary functors, thus purity is associated for each argument and not as a whole.
All pure except the last argument (implemented in this iteration) :
Only the last argument and impure applicative arguments arrow's are mandatory with
->for impurity,=>for purity.All other arrows are optional.
() => ...is valid and can be impure because()overwrite the purity annotation of the arrow.This permit writing things like :
module F : (M : S) (N : T) -> ...ormodule F : (M : S) () -> ...where we can easily expect the first argument be pure.If both argument of a functor are impure then one must write
(M : S) -> (N : T) -> ...(but such a pattern seems really rare).All arguments have the same purity :
If one uses a syntax with multiple arguments for a single arrow than all arguments will have the purity of the corresponding arrow :
Thus
(M : S) -> (N : T) -> ...is equivalent to(M : S) (N : T) -> ...and(M : S) => (N : T) => ...to(M : S) (N : T) => ...Another proposition ?