Calling a closure within a hashmap within a struct

This is still about the basics of how to access certain values within hashmaps and structs.
I tried the .get() method from the documentation, but I can't see if it the struct was returned.
And if it was, what the notation is to access the callback.


strategy.insert(String::from("strategy_id"), Strategy {

    callback: || {
        println!("{}", "Hi Callback!")
    },
});

let x = strategy.get(&String::from("strategy_id"));

x.callback()

Can you show us a simplified version of your code on the playground? (there's a "share" button in the top-right corner which will give you a link)

That way we can see the definition for Strategy and what type of closure you are using, and have some code that we can play around with.

Why do you need a playground code? This is as trivial as in JS when accessing a Map like:


const strategy = new Map()

strategy.set("strategy_id", { callback: () => {} })

const x = map.get("strategy_id")

x.callback()

okay, give me a few secs I'll do the playground code.

Because I don't have all the context that you have.

Often if I can see the exact code you are trying to run and the errors you are running into, I can understand what you are trying to achieve and how the code can be altered to get there.

For example, the code in your playground link uses a string literal for the key (i.e. strategy is inferred to be HashMap<&'static str, Strategy>) and is running into a "error[E0277]: the trait bound &str: Borrow<String> is not satisfied" error, while the code in your original post uses a String for the key and runs into a "no method named callback found for enum Option in the current scope" error.

This is why I ask for a link to the code on the playground - so I can reproduce it and we are both talking about the same code.


Assuming we are starting with the playground link, this is the error we run into:

error[E0277]: the trait bound `&str: Borrow<String>` is not satisfied
  --> src/main.rs:25:26
   |
25 |     let x = strategy.get(&String::from("strategy_id"));
   |                      --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Borrow<String>` is not implemented for `&str`
   |                      |
   |                      required by a bound introduced by this call
   |
   = help: the trait `Borrow<str>` is implemented for `String`
note: required by a bound in `HashMap::<K, V, S>::get`

That's because we did strategy.insert("strategy_id", ...) higher up and the Rust compiler inferred that strategy's key type is a &'static str (i.e. a reference to a string literal). When we call get(), it expects the value to be a &Q where K: Borrow<Q> (i.e. we can call .borrow() on our key and get something of type Q).

However &str (a reference to some text somewhere) doesn't have a Borrow impl that gives us a reference to a String (i.e. a heap-allocated object). We do have a Borrow implementation that goes in the other direction, though.

TL;DR:

let mut strategy: HashMap<String, Strategy> = HashMap::new();

strategy.insert(String::from("strategy_id"), Strategy { ... });

let x = strategy.get("strategy_id");

(playground)

(we could have also used a &'static str string literal for all our keys, but that gets awkward later on when you start needing to use keys that are only known at runtime)

That change fixed our first error, but the code still doesn't compile!

error[E0599]: no method named `callback` found for enum `Option` in the current scope
  --> src/main.rs:21:7
   |
21 |     x.callback()
   |       ^^^^^^^^ method not found in `Option<&Strategy>`

This is because strategy.get() doesn't return a &Strategy, it returns an Option<&Strategy>. The Option type is a wrapper that is roughly equivalent to T | undefined in TypeScript and indicates that sometimes the hashmap won't contain the value you want. You need to handle this in some way, but the easiest (for now) is to call Option's unwrap() method and crash the program if get() failed.

let x = strategy.get("strategy_id").unwrap();

(playground)

We now have a &Strategy stored in x, but the code still doesn't compile.

error[E0599]: no method named `callback` found for reference `&Strategy` in the current scope
  --> src/main.rs:21:7
   |
21 |     x.callback()
   |       ^^^^^^^^ field, not a method
   |
help: to call the function stored in `callback`, surround the field access with parentheses
   |
21 |     (x.callback)()
   |     +          +

Luckily, the compiler also gave us a suggestion that fixes the issue. If you read the full error message, you can also see exactly why it's complaining... The x.callback() syntax is for calling a method called callback() on the Strategy type. However, calling a function that's stored in a field requires you to explicitly say so with (x.callback)() - Rust's syntax treats field access and method calls separately!

Here is the full working code:

#![allow(unused)]

use std::collections::HashMap;

struct Strategy {
    pub callback: fn(),
}

fn main() {
    let mut strategy: HashMap<String, Strategy> = HashMap::new();

    strategy.insert(
        String::from("strategy_id"),
        Strategy {
            callback: || println!("{}", "Hi Callback!"),
        },
    );

    let x = strategy.get("strategy_id").unwrap();

    (x.callback)();
}

(playground)

And hitting "run" shows the expected output:

--- Standard Error ---
   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 3.70s
     Running `target/debug/playground`

--- Standard Output ---
Hi Callback!

Differentiating between calling a method and accessing a field which may be a function may seem a bit silly if you come from the JavaScript world, but there are actually some really good reasons to have different syntax.

First and foremost, the two forms do very different things. Calling a method is actually syntactic sugar for invoking a free function called (for example) Strategy::callback and passing in a &Strategy reference as the first argument. On the other hand, invoking a function pointer stored in a field involves dereferencing the &Strategy and reading the pointer out of the callback field, then invoking it without passing in that first &self parameter. The first form is really easy to optimise, whereas the second form involves jumping into some machine code only known at runtime.

The second reason is that it lets you create a private field called name with a public getter called name(). This way, we can prevent people from modifying our field from the outside (like you would if the field was pub), but at the same time we don't need to prefix every single getter with get_. This is a big win for both privacy and ergonomics.

struct Person {
    name: String,
}

impl Person {
    pub fn name(&self) -> &str { &self.name }
}

(playground)

3 Likes

While answer to your question is kinda trivial (you want (x.unwrap().callback)(), probably) I would strongly advise to try to lear Rust.

It's really hard to write JavaScript in Rust, C++ in Rust or Haskell in Rust. It's probably possible to write FORTRAN in Rust but even writing C in Rust is not easy.

Don't try to translate your JS programs to Rust! Or better yet, look on what other common pitfalls may await you.

It's not impossible, mind you, but just would make your work needlessly frustrating.

3 Likes

okay, but what I'm trying to do in the example is Rust, right? Not really something that is specific to JS. I'm just trying to learn about structs and hashmaps and how to access values.

Of course I'm naturally trying to translate known concepts, but I'm also aware that Rust is a completely different animal. But it's still programming.

So it is not recommended to use String::from("") as a key when doing insert?

It's perfectly fine to use String as the key for your hashmap. That lets you use strings that can only be known at runtime (e.g. because you read it from an API or used format!() to construct it on the fly).

Using a string literal (&'static str) can get a bit limiting later on because the 'static lifetime means you can only use a string literal as your keys. String literals don't require any heap allocations though because they are compiled directly into your executable, so you might want to use them under some circumstance (e.g. tables created at compile time or on memory constrained devices).

Yes and no. We ended up with valid code, sure, but it definitely looked like an attempt to create the usual (for Simula67-influenced languages) “soup of pointers” (that are connecting objects in semi-random fashion).

Rust would fight you tooth and nail if you would go that route.

First you would find out that you callbacks couldn't share state, then you would find out how to circumvent that limitation, then you would hit issues that self-referential structures are impossible and you would fight that, too.

It's long and frustrating road to learn to write JavaScript in Rust.

And at the end of that road you would have hideous monster which would leave you wondering how people may like Rust so much if it forces you to create these monstrosities.

Case to the point:

You can use String::from("") or not use String::from("") with hashmaps, but you have to decide just what do you plan to keep in that HashMap!

You may keep owned strings there or not-owned ones… you need a plan.

Yes, but that's different kind of programming. In Rust you first create a plan, teach the compiler about details of your plan and then compiler helps you to implement it.

In many other languages (especially dynamic ones) you may just sidestep the planning stage and rush to the implementation stage, try and write code and see how it works. Rust punishes that oversight quite severely.

I just want to store a callback in some sort of Struct / Object
I would probably try to do that in any programming language. I guessed that it was okay to do so in Rust.

I like to structure my Data and accessing it in various ways at various points in my code. Rust provides this possibility, or not?

What would be "the rust way" of doing this?

That's perfectly fine in Rust as you saw. Trouble comes later. Callbacks come in a four different forms in Rust. You have to understand the difference and know when to use one or another.

Just like strings come in two main forms (and half-dozen secondary forms).

You have to tell the compiler how do you plan to access that data. Who owns what? Who gives access to something and for how long?

And that's exactly the point where newbies have bloody fights with a borrow-checker.

Rust is built around ownership concept and if you don't plan your program in these terms but just try to structure your data without also structuring access patterns… there would be trouble.

I would say that typical “soup of pointers” programs and Rust programs are designed, on the high-level for opposite things.

In “soup of pointers” programs you often have intricate connections between objects and have to control “data flow bypasses” before they may render your program unsupportable.

In Rust “data flow bypass” is usually a compile-time error thus everything is kept nicely separated by default, but you have to work to connect pieces together when needed.

2 Likes

And that's exactly the point where newbies have bloody fights with a borrow-checker.
=) ROUND ONE; FIGHT!

I'm really careful already and pretty fearsome about the borrow-checker. Trust me I have only written 10 Lines of code in two days. Shit scared.

btw I need that closure to be called on websocket events. troublesome?

Is it possible to code this way in Rust?

Nah. Don't fear the borrow checker. Borrow-checker is just the Rust police. If you don't break the rules then there are no need to fight the borrow checker.

Rules are not too hard to understand and they are pretty natural. It's all about ownership. Who owns that data? Who borrows it to read? Who borrows it to write?

These are questions Rust always want to know. And if you can answer them to yourself then borrow checker is not hard to placate.

But if you write program and have no idea who owns what and how loans work… then borrow-checker becomes an issue.

2 Likes

Yes. Rust does small but important extension to that model, though.

The biggest problem in programming is shared state. Yet it also necessary because if we wouldn't be able to share changes to that forum and could only ever talk to themselves… forum would become useless, isn't it?

Data-Oriented programming solves that dilemma by pushing shared mutability to the edge, to the database.

Rust, naturally, accepts that, but it also does a tiny twist: shared mutability is bad, but excluding mutability is not the only way to solve the issue.

Another choice is to exclude shared ownership! If you are the only one who can view some piece of the data then there are absolutely no chance for anyone else to be confused if you change it!

Nobody else even have access, how can they be confused? If you are the sole owner, then you can change whatever you want.

Maybe. If your closure consumes data and that data doesn't have access from some other code — then it's easy.

If you need shared access — then it's doable, too, see RC/RefCell and Arc/Mutex.

Just don't expect to see shared mutability delivered implicitly. It's important, it's needed and often it's the whole goal of your program (think about forum, again). But usually, 99% of time, it's not what you want.

Thus every time you need it you have to ask Rust to provide it explicitly. And then, naturally, you pick the design where shared mutability is rare.

Interesting, someone else made a post on this before:

Whenever you are learning about a method, no matter the language, the first thing that you have to check (even before you type it) is its documentation. This is specially important in typed languages such as Rust (and you can get the same benefit by using TypeScript for your javascript code), since all you need is to read the function signature to understand how you should be using it.

Now, in order for someone to help you, a minimum reproducible example is absolutely necessary, specially since in many cases the way a question is formulated is misleading (what people call the XY problem).

If it was really that trivial, you wouldn't been having issues with it. One can understand that you feel frustrated when facing new challenges, but you should make an effort and make it as easy as possible for others to help you.

wow mister, don't make me look so bad. I added the playground example 1 minute after the first reply. Chill out. And no, I am not frustrated yet. Didn't even start.