Core library procedures and predicates depending on runtime data

I am trying to understand the intended usage of procedures in Odin’s standard library that take predicates as parameters.

In practice, I am running into a limitation where many of these procedures are difficult or impossible to use for common, real-world tasks. This applies in particular to procedures that expect a predicate procedure as an argument.

In the vast majority of cases, such predicates need to rely on dynamic runtime data (for example, user input, runtime state, or configuration values). This is a very common and fairly standard requirement for search, filtering, and matching operations.

As far as I understand, Odin does not support closures, so predicate procedures cannot capture external state. Because of this, I expected the standard library to provide alternative variants of these procedures that allow passing explicit user data or context into the predicate.

For example, consider slice.linear_search_proc. I expected something conceptually like this:

linear_search_proc :: proc(
    array     : $A/[]$T,
    user_data : $U,
    f         : proc(array_element: T, user_data: U) -> bool
) -> (index: int, found: bool) {
    for x, i in array {
        if f(x, user_data) {
            return i, true
        }
    }
    return -1, false
}

Without closures or an explicit user_data parameter, I do not see how predicates are supposed to be used when the condition depends on runtime data.

I am aware that context.user_ptr can be used as a workaround to pass external state. However, this feels like a rather inelegant and fragile solution for what appears to be a very common and generic problem.

So my questions are:
What is the intended pattern for using these standard library procedures that take predicates when the predicate depends on runtime data?
Am I missing some idiomatic approach or language feature related to this use case?
Is the absence of variants that accept user data a deliberate design decision, or is this part of the core standard library still evolving and expected to be extended?

If I understand correctly what you are driving at…

I would actually refer to those types of procedures as parametric polymorphic procedures. The $THIS and $THAT are basically compile time constant macros that allow for defining dynamic specialization, i.e. runtime variant types.

Karl Zylinski describes the usefulness of these quite well. IMHO opinion this is very useful. My only complaint is the core and base libraries have already covered most of the use-cases, so I don’t get to use them as often as I’d like.

https://www.youtube.com/watch?v=3X2IzOfzepA

So take the original definition of linear_search_proc. It allows to define your own callback proc pointer for any given type, and give it to the procedure. See how in this case I have an overloaded procedure that I use to define which type variant is chosen by linear_search_proc.

my_array1 := []int{1, 2, 42, 3}
my_array2 := []string{"1", "2", "42", "3"}

my_proc :: proc {my_proc1, my_proc2 }
my_proc1 :: proc(i: int) -> bool {
	if i == 42 { return true }
	return false
}

my_proc2 :: proc(i: string) -> bool {
	if i == "42" { return true }
	return false
}

index1, found1 := slice.linear_search_proc(my_array1, my_proc)
fmt.printfln("index: %v, found: %v", index1, found1)

index2, found2 := slice.linear_search_proc(my_array2, my_proc)
fmt.printfln("index: %v, found: %v", index2, found2)
1 Like

This isn’t exactly the case I’m talking about.
In your example, 42 is a constant, so there’s no issue there. All the procedures from the core library work fine for that.

What I’m referring to are situations where the number isn’t known in advance. Like when it comes from user input or is read from a file. In those cases, I don’t see how to use the procedures from the library. And in practice, most of my cases fall into this category.

print_index_of_number :: proc(array : []int, number : int) {
    index, found := slice.linear_search_proc(array, ???)
        
    if found {
        fmt.println(index)
    }
}

Writing a procedure to solve my problem isn’t difficult.
The issue is that this is a fairly common case, yet I can’t find a ready-made solution for it.
In practice, I’ve found that most of the procedures in the core library are of little use to me.
I’m trying to understand whether this is by design or if I’m missing something.

English is not my native language, and I’m using tools to help me write. I apologize in advance if I’m not able to express my thoughts clearly.

There’s this procedure that will allow a search of any number of the same type in the array. Is there a reason you are focused on linear_search_proc and not using linear_search?

my_array1 := []int{1, 2, 42, 3}
my_array2 := []string{"1", "2", "42", "3"}

var1 := 42
var2 := "42"

index1, found1 := slice.linear_search(my_array1, var1)
index2, found2 := slice.linear_search(my_array2, var2)

fmt.printfln("%v %v", index1, found1)
fmt.printfln("%v %v", index2, found2)

I’m not focusing on a specific method here. I only mentioned linear_search_proc as the simplest possible example.

I think there may be a misunderstanding. Let me try to give a clearer example.

Consider a search scenario where I need to find the first value that matches a certain condition. The values used by that condition are not known at compile time.

For example, I want to find and print the name of the first user who satisfies an age requirement. The age limit is obtained from an external source, so it is only available at runtime.

User :: struct {
        name : string,
        age : u32,
}

fetch_age_limit :: proc() -> u32 ---;

print_first_user_above_age_limit :: proc(users : []User) {
        age_limit := fetch_age_limit()
        
        index, found := slice.linear_search_proc(users, ???)
        
        if found {
                fmt.println(users[index])
        }
}

This is just one example. I’m not interested in how to solve the problem in general — I can always write my own search procedure.

What I’m trying to understand is how to handle similar cases using the existing procedures from the core library. The question is not about searching itself, but about using predicates with dynamic conditions.

At the moment, it looks to me like this is not possible.

So essentially my question comes down to whether:

  • I’m missing some feature or idiomatic pattern in the language,
  • the library is not complete yet and this use case is not supported,
  • or the design intentionally keeps the core library limited to very primitive functionality

I assume that I’m not missing anything, since I can see that some procedures already use a user_data pattern. For example, slice.sort_by_with_indices_with_data.

What I don’t quite understand is why this pattern is used in some places, but not in others.

It’s possible that my initial expectations for the core library were too high — or more precisely, for the slice package. I was expecting something similar to C++’s std::algorithm, or at least a subset of it.

However, my impression is that the library doesn’t provide everything. In some areas the coverage feels more complete, while in others it’s only partial, and overall it seems somewhat inconsistent in terms of typical use cases.

Does this help? Same problem?

User :: struct {
	name : string,
	age : u32,
}

fetch_age_limit :: proc() -> u32 {return 42}

print_first_user_above_age_limit :: proc(users: []User) {
	index, found := slice.linear_search_proc(users, proc(u: User) -> bool {
		if u.age > fetch_age_limit() { return true }
		return false
		}
	)
	if found {
		fmt.println(users[index])
	}		
}

main :: proc() {
	users := []User{{"dana", 30}, {"xuul", 50}, {"vinz", 30}}
	print_first_user_above_age_limit(users)
}
1 Like

Thanks for your suggestions! I appreciate your effort, but I think there’s a misunderstanding. Your solution seems to address a different problem than the one I originally asked about. I probably didn’t manage to fully convey the point of my question.

The misunderstanding is most likely on my part. I’m better in practice than theory.

That said, my example does illustrate at least the intended purpose of the predicate for the linear_search_proc procedure. Maybe you can extrapolate that to other procedures you wish to use.

If you give me a real world problem you’re trying to solve, I’m better at giving answers for that. I’ll leave the theory part to others smarter than me.

Ok, I think I’ve a better clue what you are talking about. There’s a binary_search_by_proc that allows for a variable key, but not a version for linear_search. It seems that has been left out for some reason. Technical, oversight, or unfinished, I could not say. But, I think I understand what you were getting at now.

So, here’s the thing. I’ve seen mentioned in the forums, that if a procedure does not exist for you, to use one that does and make your own. So, using binary_search_by_proc, I made a linear_search_by_proc, that allows for a variable key. Am I getting closer?

User :: struct {
	name : string,
	age : u32,
}

fetch_age_limit :: proc() -> u32 {return 42}

linear_search_by_proc :: proc(array: $A/[]$T, key: $K, f: proc(T, K) -> bool) -> (index: int, found: bool) {
	for x, i in array {
		if f(x, key) {
			return i, true
		}
	}
	return -1, false
}

main :: proc() {
	age_limit := fetch_age_limit()
	users := []User{{"dana", 30}, {"xuul", 50}, {"vinz", 30}}
	index, found := linear_search_by_proc(users, age_limit, proc(u: User, age: u32) -> bool {return u.age > age})
	fmt.printfln("%v, %v", index, found)
}

Not quite.
If you look at the code example in the very first post, you’ll see it’s the same code you wrote :smile:.

My question isn’t about solving a specific problem.
I’m trying to understand the reason behind the inconsistencies in the API provided by the core library, particularly the slices package.

I want to know whether these inconsistencies are:

  • intentional, meaning there’s a specific way to work with only what’s available,
  • a deliberate design choice, where the expectation is that the missing functionality should be implemented by the user, or
  • simply unfinished.

Good question. I have no clue. I only know the answer to the ultimate question that is unknowable, which is 42. Besides that, I did learn something, so thank you for walking me through your question.

Hopefully someone has an answer for your question (looking at Bill :xmasbeerbill:)

1 Like