Implicit Parameters

If Rust ever gets keyword arguments, a mechanism to pass the same value to all applicable function calls in a block might serve this use case reasonably:

fn from_context(idx: usize, keyword ctx: &Context)->i32 { ... }

fn example1(ctx: &Context) {
     let keyword ctx = ctx;

     dbg!(from_context(3));
     dbg!(from_context(7));
}

// Equivalent to:

fn example2(ctx: &Context) {
    dbg!(from_context(3, ctx = ctx));
    dbg!(from_context(7, ctx = ctx));
}

(NB: This isn't a real proposal; the syntax here is probably terrible in many ways)

Surely this is only true if the implicit parameter is a impl T parameter or otherwise generic? If the implicit parameter is just an integer I don't see why this would be necessary?

Yes, an implicit i32 would not require monomorphization. But given a key motivating use case is implicit allocators, my point still stands.

1 Like

I presented the idea as if it was a generic feature, but local allocators were the primary motivation.


This is where I currently stand on the issue, I think a concept of implicit arguments should be conditional on default arguments. And if these features do ever manifest I think implicit paramaters could just simply wire new default arguments.

// Assume Box::new takes optional arg allocator that defaults to the global alloc
fn foo(){
   let implicit allocator = Bump::new()
   Box::new(Foo) // implicitly passes bump allocator to optional arg
}

This wouldn't solve the fragmentation issue, but it could make it simpler for library authors to add support. I could see a macro like #[implicit_allocator] that libarary authors could manually add to their functions to add support. This also ensures no API/ABI are changed implicitly.

#[implicit_allocator]
fn foo() { ... }
// becomes (I'm not sure what the default syntax looks like)
fn foo<A:Allocator>(allocator : implicit &A default GlobalAlloc) { ... }

Maybe by being opt-in like above this wouldn't be such an issue. I suspect that most Allocators will actually be passed by reference, maybe for debug builds the #[implicit_allocator] macro could get away with just using a &dyn Allocator and only compile using a <A : Allocator>(&A) functions on release.

I sketched something similar in my head at one point.

The way I imagined it, modules could declare a new kind of item, a symbol (name up to change; associations with symbols in Lisp or JavaScript may or may not be helpful). A symbol would associate a type with a name, kind of like a static; unlike a static, it would not create any storage.

mod foo {
    pub sym bar: u32;
}

Now, any code in which the symbol is visible would be able name an existing symbol when creating a binding instead of creating a new identifier:

let sym foo::bar = 1;

The presence of such a binding would allow you to refer to the symbol path as if it were a variable, and would refer to the symbol binding in the innermost scope. Shadowing of symbol bindings would of course be possible as usual.

It would also be possible to declare symbol bindings in argument lists:

fn quux(sym foo::bar) -> u32 {
    foo::bar + 1
}

Such a function would be callable only in scopes in which there is a binding pointing to the symbol, and would pass the value so bound.

println!("{}", quux()); // error: no symbol binding for foo::bar in scope
let sym foo::bar = 5;
println!("{}", quux()); // OK: prints 6

The presence of a symbol in a parameter list would of course create a symbol binding available to any callee:

fn xyzzy(sym foo::bar) -> u32 {
    quux() * quux() // OK
}

In effect, this would create a kind of quasi-dynamic scoping, but checked statically. Namespacing symbols in modules would avert the homograph problem that might otherwise result (what happens when two functions want symbol bindings with the same name but disagreeing types).

I did not pursue this idea further though, as I wasn’t sure if it pulled its weight and could be made ergonomic enough.

1 Like

Please forgive this post for being a bit tangential. To me, this whole idea of implicit parameters bears a strong resemblance to 'algebraic effects' (Haskell) or 'monadic effects' (Scala) or 'abilities' (Unison). Though Unison is the most 'upstart' of these languages, I like its conception of this idea the best. I'll try to summarize Unison's take here, but to be honest I only have a shallow understanding.

Edit: I forgot to mention that the Unison creators attribute their implementation of effect handlers to ideas that came from the Frank language, which is explored in more clinical detail in the Frank language authors' paper titled Do Be Do Be Do (yes, actually).


Unison is a pure functional language, so they also run into the classic "but how to IO?" problem. Unison's solution repackages monadic effects into "abilities" and "ability handlers". Practically, this looks like a second set of parameters ("abilities") declared on functions and has some slightly different rules for how the parameters propagate up to definitions implicitly and how such functions compose. Abilities themselves are declared like interfaces/traits, but they have more access to the control flow during execution. Here's an expanded excerpt of Unison's docs on abilities:

Here's a simple pure function that doesn't use abilities / do any fancy IO:

// impl of `msg` concatenates the Text arg name to construct another Text value
msg name = "Hello there " ++ name ++ "!"

Its definition would be:

// `msg` is a function that takes a Text and returns a Text
msg : Text -> Text

This function uses msg defined above to do some IO (print to stdout):

greet name = printLine (msg name)

It's definition would be:

// greet is a function that takes a Text and returns Unit (nothing), but also requires the IO ability (the `{IO}` part)
greet : Text ->{IO} ()

msg could also be declared as msg : Text ->{} Text which is identical to the previous definition except it explicitly declares that it accepts no abilities.

Abilities 'propagate up' definitions in a call chain. E.g. a function greetMs name = greet ("Ms " ++ name) would inherit the IO ability in its definition implicitly despite not using IO directly: greetMs : Text ->{IO} ().

A function can require more than one ability, e.g. in someFn : Text ->{IO, Abort} (), someFn is like greet but also requires the Abort ability.

A function can "handle" an ability in a function that it calls, which enables it to avoid requiring that ability in callers.

(Imagine I put an example here that calls `greet` with a handler for IO that puts calls to printLine 
in a buffer and returns the buffer, and thus its definition doesn't have to include `{IO}` in its list
of abilities.)
(sorry I don't know how to do that, and I suspect it would get a bit too far into the weeds for this post)

Now I think that Abilities are pretty neat and seems to be related to the "Implicit Parameters" being discussed here, but I have no idea how they would relate to a real Rust implementation and I'm not making any specific recommendations, I just wanted to connect this thing to that other thing just in case we can learn something from other work in this space.

Here are (probably too many) resources I found that discuss abilities in Unison:

Video content:

5 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.