Those-Which-Must-Not-Be-Named (i.e., everything we can't name)


#1

I’ve seen a few threads lately trying to come up with syntaxes for unnameable entities. I figured I’d make a complete list of everything that is currently unnameable, so that we can try to deal with it all at once. This is all in absence of typeof, of course.

This is an incomplete list based off my excellent^W iffy memory. Let me know if I missed anything!

Types

  • The type of a function.
fn foo(_: T) -> U { .. }
// problem: I can't name the ZST that the following types as:
let bar = foo;
// workaround:
type foo_t = impl Fn(T) -> U;
let bar: foo_t = foo;
  • The type of a fn-local item (struct, enum, type, etc)
// problem: I can't name `???` outside the fn scope
fn foo() -> ??? {
    struct Foo;
    Foo
}
// workaround:
struct Foo;
fn foo() -> Foo {
    Foo
}
  • The type of a closure.
fn foo() -> impl Fn() -> i32 {
    || 0
}
// same problem as the type of a function, really
  • Output of a function.
fn foo() -> i32 { 0 };
// problem: how can I name this type without naming it again?
let bar = foo();
// workaround via assoc. type
type foo_t = impl Fn<()>;
let _: foo_t = foo;
let bar: foo_t::Output = foo();

Values

  • Fn-local item (fn, const, static).
fn foo() {
    fn bar() {}
}
// problem: how do I name bar outside of foo?
let baz = foo::bar;
  • The captures of a closure. One can argue that those can’t be made pub though.
let a = 0;
let b = 1;
let bar = move |x| x + a - b;

let mut baz = bar.clone();
// problem: how do I do this? (assuming the field's 
// name is the same as the capture's)
baz.b = 1;
// workaround: transmute! (or ptr casts) this is unstable though
// and maybe even UB!
unsafe { *transmute::<_, *mut i32>(&mut baz).offset(1) = 1; }

Lifetimes

  • The lifetime of a function’s body.
fn foo(x: T) {
    // problem: what lifetime goes here? sure, we can let the compiler
    // infer it but relying on inference to name a lifetime
    // can lead to problems if inference isn't allowed in some palce
    let foo: &'? T = &x;
}
  • The lifetime of a struct.
struct MyBox<T: ?Sized> {
    // problem: what lifetime goes here?  I need to use `*mut T` instead, 
    // since I can't just say "this pointer lives for as long as this
    // this struct does" (we don't have &move yet but my point stands)
    ptr_to_heap: &'? move T,
}

Solutions

The following are some ideas I’ve had/seen for solving each of these.

  • Type of fn: fn foo or foo::type.
  • Type of fn-local item: (fn foo)::Foo, foo::type::Foo, foo::Foo.
  • Type of closure:
// introduce a new type inside the closure by ascribing the closure.
// there's a lot of unresolved problems with this, so it should probably
// be discussed in its own thread
fn foo() -> fn::Closure {
    (|| 0): Closure
    // or
    let closure: Closure = || 0;
    closure
    // or maybe denote that it's a new type with special syntax?
    let closure = struct Closure || 0;
}
  • Fn output: foo::return? (fn foo)::return? Could also imagine foo::yield for generators.
  • Inner fn: same as inner type.
  • Closure captures: probably should be described in its own thread.
  • Fn body lifetime: 'fn.
  • Struct lifetime: 'self.

PS: There’s one slightly insane corner case, where it’s impossible to name a lang item (like one of the panic fns) if you don’t know what it’s actually called. Imagine the following:

#![no_std]

fn my_panic() -> ! {
    __lang__::panic_fmt()
}

I don’t think this is actually useful in any way, but I figured I’d mention it.


#2

Functions do have a type we can name:

fn foo(_: T) -> U { .. }
let bar: fn(T) -> U = foo;

Agree with everything else here though!


#3

Not quite. fn(..) -> T is a function pointer, which has size mem::size_of<usize>(). Each function has an associated ZST, which is best illustrated by this playground link. The following code might also convince you:

fn foo() {}

let f = foo;
let g: fn() = foo;

println!("{} {}", mem::size_of_val(&f), mem::size_of_val(&g));

#4

In my opinion this is not a problem. The user has clearly intentionally hidden bar inside foo. For all intents an purposes, foo then works as a module. I don’t think this should change. Furthermore, it might not actually work if you introduce scoped type variables.

Absolutely UB. You are not guaranteed that b is at offset 1 and so the behavior is undefined.


#5

I didn’t include this because I wanted my examples to all be syntactically correct, but imagine if I wrote

fn foo() {
    pub fn bar() {}
}

Can’t deny that I want the world to see foo::bar now! This came up in another thread about fn-local items.

Yep, I always forget that transmutation of non-#[repr(C)] types is UB!


#6

Heh. Link to the thread?

fn foo<T>() {
    pub fn bar(x: T) -> T { x } // Using ~ScopedTypeVariables from Haskell
}

And now we have parameterized modules, foo<u8>::bar(1) :stuck_out_tongue:


#7

#8

Another thing came up when writing the Formalise Reborrows RFC (postponed):

trait Reborrow {
    // Returns Self but with fresh lifetime parameter(s)
    fn reborrow(&self) -> ???;
}

impl<'a, T: 'a + ?Sized> Reborrow for MyRef<'a, T> {
    fn reborrow<'b>(&self) -> MyRef<'b, T> where 'a: 'b {
        MyRef(self.0)
    }
}

#9

So my knee-jerk reaction is "oh what you totally want is

// making up syntax to say "Self must have kind `lifetime -> type`"
trait Reborrow where Self<'a> {
    fn reborrow<'b>(&self) -> Self<'b> where 'a: 'b;
}

// and type closures, too
impl<T: ?Sized> Reborrow for |'a| MyRef<'a, T> where T: 'a {
    fn reborrow<'b>(&self) -> Self<'b> where 'a: 'b {
        MyRef(self.0)
    }
}

Though… maybe what you want is say the opposite of T: 'a? Like… 'a: T, 'a outlives all lifetimes in T?

trait Reborrow {
    fn reborrow<'a>(&'a self) -> Self where 'a: Self
}

Or maybe something like a “lifetime intersection” T & 'a, replace all lifetimes 't in T with some 'fresh where 'a: 'fresh, 't: fresh (i.e. the intersection lifetime)?

Let me know which of these matches your thinking.


#10

Is a knee-jerk reaction useful? First, you can’t write Self<'a> today. Second, there might be more than one lifetime involved. I only mentioned the problem here because you seem to be collecting them.


#11

Oops, apparently part of what I wrote down got cut off! What I meant to say is that I think your problem could be solved by higher kinded types, if we got them. It sounds to me like you want a way to express a type operation that forces all contained lifetimes to be shortened (see my T & 'a note), rather than to name a type or value that the compiler already reasons about but that isn’t pronounceable in source code.


#12

Basically, yes. I was able to test the implementation above, but obviously not the trait. The most tricky thing is supporting multiple lifetime arguments and placing bounds on each. I haven’t been following HKT development so I’ve no idea if there’s anything on the horizon capable of this?