Rust vs. Haskell(serokell.io) |
Rust vs. Haskell(serokell.io) |
Most Haskellers I know are quite fond of Rust :)
Of course Haskell is heavily influenced by ML so it also has a lot of the same features.
Haskell has a garbage collector for a reason.
You can't program naturally with closures unless you have a garbage collector, because closures introduce cyclic references.
Also, Haskell has a type inference system that allows e.g. the IO monad to work seamlessly with the rest of the language.
EDIT: and laziness of course.
There's a pareto frontier of "best available language for a given project", and I think Haskell dominates the portion of the pareto frontier where you don't have super tight physical/memory/real-time constraints, and Rust dominates the portion where you do. This is by virtue of their similarity.
Rust just happens to heavily use linear types. Haskell let's you use both. If you write your Haskell program using linear types you get borrow checker semantics.
The main difference is mem management. However haskell could likely be written to be manually memory managed. There are manual men based ocaml implementations.
Haskell is not typically used for systems programming.
> you don't even have strict order of evaluation!)
People say this but I'm not sure people understand it. Haskell evaluation order is exotic but deterministic in all cases unless you introduce threads, which bring non determinism into any language including rust
You do, with `seq`. And a lot of Haskell programming in practice is deciding between lazy vs strict programming.
The only lazy feature commonly used by Rust programmers are iterators, but they are just lazy lists (that are immediately discarded after being forced)
Well yes and no. In a way the features such as immutability and algebraic data types are things you should know about as a software developer even if your current language means you can't use them at the moment.
My 16 year old son has learned Rust coming from a python at school background and is now writing small games in the bevy game engine.
Having to understand monad transformers or another kind of effect system to get anything working is a heavy load that's unnecessary in other languages.
I'd say knowing how to use monad transformers is the barrier between intermediate and expert Haskell programmers.
Effect systems and monad transformers are advanced topics. They're possible to use in my language yet most people do not and can still write software.
Certainly no need for them in Haskell to be successful. You can just program at the lower levels of abstraction common in other languages.
The only difference is that Haskell's community tends to emphasize abstraction
"The common expression "a steep learning curve" is a misnomer suggesting that an activity is difficult to learn and that expending much effort does not increase proficiency by much, although a learning curve with a steep start actually represents rapid progress."
https://en.wikipedia.org/wiki/Learning_curve
When writing, consider "challenging learning curve" instead (I'd love other suggestions too).
Rust struct and Haskell records work pretty much the same way, too:
// Rust
Item { name, price }
Item { name: name, price: price }
match item {
Item { name, .. } => todo!(),
}
corresponding to -- Haskell
Item { item, price }
Item { item = item, price = price }
case item of
Item { name, .. } -> undefined
Haskell has some opt-in flexibility wrt. packing and unpacking of field names [1].[1]: https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/reco...
// rust
let Item { name, .. } = item;
-- haskell
let Item { name } = item in …
(Note: the haskell version requires NamedFieldPuns, or RecordWildCards for something like Rust’s version)Another possible gotcha is that by default Haskell records introduce a named accessor function into the surrounding scope. So defining two records with a `name` field next to each other is an error
This is quite aptly expressed by the recent paper on dependent types [1]:
> Previous versions of Haskell, based on System Fω , had a simple kind system with a few kinds (, → * and so on). Unfortunately, this was insufficient for kind polymorphism. Thus, recent versions of Haskell were extended to support kind polymorphism, which required extending the core language as well. Indeed, System FC↑ [30] was proposed to support, among other things, kind polymorphism. However, System FC↑ separates expressions into terms, types and kinds, which complicates both the implementation and future extensions.
Later in the paper, they show how some of the most recent "fancy" features of Haskell can be achieved in a more economic framework based on first-class types. Unfortunately, systems programming (as in C/C++) puts strict constraints on a programming language, and currently it's not quite clear what's the best way to integrate dependent types with systems programming. In the coming years, I expect to see some forms of castrated dependent types tuned for systems programming (e.g., types dependent only on indices).
[1] https://www.researchgate.net/publication/308960209_Unified_S...
Dependent types dispense with the phase separation between compile-time and run-time code, which is inherent to system languages. So you can easily have dependent types in a systems language as part of compile-time evaluation, but not really as a general feature. It would work quite similar to the "program extraction" feature in current dependent-language implementations, which does enable improved type checking because you can express arbitrary proofs and check them reliably as part of compilation.
What black magic is this? Is the article just glossing over the cost of a copy or does Haskell do something weird here to avoid the copy while retaining both versions?
I prefer Rust's way of doing this. When you're a beginner it ensures that you're passing and returning the proper type to and from the function, you kind of have the function as a guard which ensures that you're using the correct types.
I do not want abstractions where they aren't needed. I want control, simplicity, a clear correspondence between what I'm writing and what logical assembly I'll be generating (logical, not physical). Most of all I want my code to be stupidly clear to the next person reading it. Systems programming isn't like writing a Java app, the domain is complicated enough that there's no room for abstraction astronauts to create problems where there are none.
I am still very wary of Rust. I have used it and will continue to use it, but it still teeters on being too complicated to be useful in the same way as C.
> and what logical assembly I'll be generating
The problem with Rust and C is not worrying about what assembly I'll get, it is that one has very little control over the layout of the stack. It's the last implicit data structure and that's a PITA for highly resource constrained programming.
I absolutely need to worry about what assembly I get. I am often checking my assembly or looking for optimizations in my assembly. And I'm doing this across multiple architectures.
So it doesn't have the simplicity of C, it tries to give you as many abstractions as possible while still maintaining the zero cost philosophy.
I would say Rust is easier then C++ and easier haskell.
- OCaml is definitely tailored for more "high-level" tasks, such as writing a programming language or a theorem prover (there are many of them in the OCaml world, and even Rust was initially written in OCaml, as you've mentioned). OCaml has a GC, which might be a problem under certain circumstances.
- Rust has a far better ecosystem despite being a younger language. You can just compare the number of packages on crates.io and OPAM.
- OCaml _sometimes_ has some more fancy type features, such as functors (modules parameterized by other modules), first-class modules, GADTs (Generalized Algebraic Data Types), algebraic effects, and the list could go on. It doesn't have type classes or an ownership system though.
- OCaml is more ML-like, while Rust quite often feels C-like. For example, you have automatically curried functions in OCaml and the omnipresent HM type inference.
- OCaml has a powerful optimizer called Flambda [1], which is designed to optimize FP-style programs.
Having written some code both in OCaml and Rust, I can say they are in a lot of aspects quite similar though. They both take a more practical POV on programming than Haskell, which affects language design quite evidently.
Also, Rust's idiomatic type system usage is way closer to Haskell's than it is to OCaml's (with typeclasses, no polymorphic variants, no module functorization, etc.)
[1] https://old.reddit.com/r/haskell/comments/zk2u6k/what_do_has...
let nums = take 10000000 naturals
print $ (sum nums, length nums)
> Because the nums list is used for both sum and length computations, the compiler can’t discard list elements until it evaluates both.Now that makes me wonder, if I write something like
print $ (sum (take 10000000 naturals), length (take 10000000 naturals))
will it run in constant memory? I think it ought to, but are there mechanisms in GHC optimizer to prevent extraction of CSEs that would cause huge increases in memory consumption?Most important question about Haskell
I participated teaching students Haskell on my alma mater this year and "what can Haskell be used for" was a common question, with genuine expectation that the answer will be it is limited to only specific use cases. I would answer that it can be used anywhere where languages like Java, C#, Go, and similar can be used -> it is a general programming language that uses garbage collector! And while somewhat harder to learn due to abstractions that we are all not used to, it is a delight to express business logic in it once you get to know it well.
The biggest factor for deciding if Haskell is a good fit for the problem is probably ecosystem support -> are there enough libraries and tools to support efficient development in a specific problem domain. In our case, we are building a compiler/transpiler, and Haskell is well-known for great support in that area, so it was a no-brainer. We were actually also considering Rust, but we just had no need for that level of memory control and rather decided to go with language where we don't have to think about that (Haskell).
Conversely, in the case where something like a list is modified in entirety (e.g. with a `map` function), if the compiler can determine that the original is no longer needed, it can run the map operation in place - much like you might do on an array in C - avoiding the need for a second copy of the structure in-memory.
From https://en.wikipedia.org/wiki/Persistent_data_structure
Clojure leverages the same data structure for (at least) four basic types; list, map, vector and set, and the following article explains it well with a pretty graph/picture too: https://practical.li/clojurescript/clojure-syntax/persistent...
For example, in a basic binary search tree implementation of a map (using C-ish syntax for those who don't know a functional language):
struct Node {
String key;
int value;
Node left;
Node right;
}
Node set(Node n, String key, int value) {
if(n == null) {
return new Node { key = key, value = value, left = null, right = null };
}
if(key < n.key) {
return new Node {
key = n.key,
value = n.value,
left = set(n.left, key, value),
right = n.right
};
}
if(key > n.key) {
return new Node {
key = n.key,
value = n.value,
left = n.left,
right = set(n.right, key, value)
};
}
return new Node { key = key, value = value, left = n.left, right = n.right };
}
A perfectly-balanced tree with depth of 5 has 1 + 2 + 4 + 8 + 16 = 31 nodes. If you call the above function, on such a tree, the worst case scenario is that the key doesn't exist, so it modifies 5 nodes during its search and creates a new sixth node. 26 of the original 31 nodes are reused and referenced by the newly-created map. The percentage of nodes reused only improves as the perfectly-balanced tree gets larger.Of course, if this is your implementation of set(), the tree won't be perfectly-balanced, so a production implementation of a tree-based map needs tree-rebalancing (as well as memory-reordering and compacting for cache locality). These extra constraints typically mean less of the tree can be re-used, but the percentage of nodes which can be reused remains high.
You have an object. When you want to apply a patch, you create a new object that contains just your patch, plus a reference to the old obejct. DiffArray is the simple common example. It's fast enough when the diffs are small, but terrible when there are many diffs in series, creating a deep stack of references.
It's not obscure. $PATH and the /bin,/usr/bin, /usr/local/bin, $HOME/bin dirs on Linux work the same way.
And GHC exploits that liberty.
You don't need it for all function declarations though, there are many trivial cases where type signatures don't add value. Consider that you probably would prefer most of the variables within a function to be inferred for you by the compiler. A similar thing could be said about the most of the functions within a given module. Important interface methods can be defined explicitly though, for extra clarity and self-documenting purposes.
module functions: if I chose, I'd let them be derived, but I'm relatively indifferent
inner functions: should be allowed to be derived, IMO. They're very infrequently used so this choice doesn't have much impact.
It's certainly good practise to write out your types, and often your editor can do it for you, but there are tons of little places where it's a waste.
Lifetime annotations aren't elided based on there being only one unambiguous answer. Rather, there's three simple rules[1] that look at the function signature and in simple cases give what you probably want. If those simple rules don't match your use case you need to define manually, the compiler doesn't get clever.
This means that if you want to take a reference to an array and return a reference to an item in an array, for example, elision will work fine. But if you take a reference to a key and look up a reference to the value in a global map you need to write it by hand, even though the compiler could pretty clearly guess the output lifetime you want.
This preserves a crucial feature: you can know the exact signature of a function by only looking at the signature definition, you don't need to look into the body.
Lifetime elision isn't like inferring argument types. It's like defaulting integer literals to i32 unless you specify otherwise.
When you say 'algorithms would look better' this is pretty subjective. Graph based algorithms or inplace algorithms doesn't look too good in haskell.
Because it’s target domain requires reliable efficiency.
> Wouldn't it be better if we could just have the compiler guarantee that functions marked as such
As such what?
> are doing TCO
TCO is worthless, and Rust does not have TCE, nor does it care about TCE.
> so algorithms would look better and had the full benefit of persistent data structures?
Rust does not significantly care for persistent data structures, there are crates which provide them, but not the standard library.
Rust is not a functional language.
Why not? Why this "there can only be one" mentality?
Because you can't simply disallow mutability in an imperative language.
But about annotations, you annotate your code to let the compiler know what you meant to do. If just looking at what you do was enough, C would be a safe language. You improve it by giving more information to the compiler, so it can check if you are doing what you meant to.
(But I don't know what relation you saw with TCO. It's not a related concept.)
Maybe because it's designed to replace existing, familiar imperative and mutable languages.
Well you can use Rc/Arc (that’s what Bodil’s imrs does for instance), but essentially yes, by design and purpose a persistent data structure has confused ownership, and thus requires a separate layer handling memory reclamation.
And hiring right, or being prepared to train/let new hires climb up a steeper learning curve than hiring someone with Python experience for a Ruby app say.
Rust has reached that critical mass I think, got past the chicken/egg issue of experienced people to hire & companies interested in hiring them (to work on a rust codebase). Against the odds I think, there are plenty of languages you hear about similarly up and coming that haven't (D, Zig) or have only in a niche (F#, Swift, Kotlin - the last two I include mainly because I'm thinking Go could so easily have gone the same way, just been the one Google pushed for K8s plugins and GAE applications, not used generally as it is despite being a general-purpose language).
How is Wasp more convenient than other frameworks? How do I hire people that know it or can train themselves to use it?
Then the power of good abstractions should be all the more useful!
> that interact directly with hardware
OK if you are using various privileged instructions, writing things with weird calling conventions like interrupt handlers, etc. Then the assembly matters.
But, and this may sound heretical, this stuff is the boundary of the lower level code; its interface with the hardware. For the interior, the precise assembly once again doesn't matter.
> And I say this with many years of experience dealing with other companies badly written device drivers and firmware.
There is a lot of crap low level code out there, yes. And I would say a chief mistake of crap code is not properly respecting layers of abstraction. The low level world needs more http://langsec.org/ where we precisely and mechanistically document the interface, rather than half-assing it with reference implementations.
Just as user space weird instructions get intrinsics exposed to spare the user from writing inline assembly, libraries like https://docs.rs/x86/latest/x86/ should be maintained the device manufacturer or ISA consortium. They should be unit-tested with an emulator too.
We properly do all this legwork to fix the foundation, and the rest of the low-level software will practical write itself, compared to today, and be accessible to a much larger pool of programmers.
The old alpha/beta rust docs specifically referenced Haskell all over the place, and I don't recall them mentioning OCaml.
> OCaml feels a lot more similar to Rust than Haskell does
I disagree very strongly with this. OCaml doesn't even have type classes/traits! Rust lacks polymorphic variants, functorial modules, etc. It's really nothing like idiomatic ocaml, whereas I think it's pretty reasonable to describe Rust as "what Haskell devs would make if they weren't allowed to heap allocate". Maybe they'd also add a few more gizmos to the type system :)
The big thing IMO that makes Rust feel like Haskell is the pervasive use of traits and `#[derive(...)]` which is directly analogous to the pervasive use of typeclasses and `deriving` in Haskell.
That's because the entire haskell ecosystem would have to be rewritten which seems insurmountable.
As for getting somebody on-board -> we hired a couple senior / intermediate devs that had no or introductory knowledge of Haskell, and all of them so far got up to speed in a month or so, while not learning exclusively Haskell but also the rest of the codebase at the same time, so normal learning process in the new company. So I wouldn't say at all that learning Haskell for them was an issue, but I am certain that big factor here was that they are generally experienced in other languages. That said, we do keep our codebase pretty tidy and simple (no super crazy Haskell features).
If you add prepend an element to an existing list, no copy is necessary. It's just a new head with the tail being the original list. That's an easy case.
If you add a node in the middle of the list you can still share the unchanged tail between the two slightly different prefix sequences.
The simplest example would be with linked lists, I suppose: you have a list A->B->C, then you take the B->C sublist and prepend X to it, so you now have X->B->C, but A->B->C is still around, and "B->C" part is shared between those.
E.g. let's say you create a map with keys A and B. You then insert a new key C. In memory you might have two objects: One is the origi al map with A and B and the other has "Key C and a ref to the first map".
The speed of C++ with high level language features of haskell. And none of the safety pitfalls of C++ either. That's a lot.
When compared with haskell the benefits aren't clear. When compared with C++, rust is clearly better when viewed from strictly a language standpoint.
The strongest argument for Rust is its safety features. Abstractions have made C++ an unusable mess that even C++ developers complain about on a constant basis.
Note that in my example the type information flows from the variable to picking which generic method that is called, which is reversed information flow of what I would call the most trivial case - when the variable gets its type from the method that was called. (This is trivial: let x = 1i32;)
> C++ doesn't support it
How exactly is it different?
#include <iostream>
#include <vector>
int main() {
std::vector<int> v1 = {1,2,3};
auto it = v1.begin();
std::vector v2 (it + 1, it + 2);
for (auto i: v2) { std::cout << i << ' '; }
}(Rust has your C-like run of the mill function references too. People should emphasize those more, because differently from closures, those work very well.)
In contrast, function pointers are very rarely used.
Are they equivalently nice in every way to closures in Haskell? Of course not. But I think your comment is swinging way too far in the opposite direction.
OTOH, things like parser combinators are much more ergonomic in Haskell than Rust.
[1] https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-...
The point is that the borrow checker forces you to program in a fundamentally different style than idiomatic Haskell.
How do you think the folks on Java and C# find their work worth doing on their end in that case?
No, TCO is worthless, it’s an unreliable optimisation.
Context is relevant, the context here is language semantics, not things going faster.
TCO is fine to make things go faster, like inlining, unrolling, LICM, ....
TCO is hot garbage when you rely on it for program correctness.
Do you believe the exec system call is similarly unreliable at replacing your process image? Amazing that unix systems work at all
Then it's TCE not TCO.
> Do you believe the exec system call is similarly unreliable at replacing your process image?
Exec is entirely unlike TCO. If exec was a syscall which sometimes did and sometimes did not replace your process image, then it'd be like TCO, and it'd be similarly unreliable.
Rust controls mutable data by the borrow checker system: allow sharing OR mutation but not both at the same time.
First the type system is very similar with the capability of building sum and product types with recursive values. Second pattern matching over sum types is also very very similar.
Rust is like the bastard child of C++ and haskell.
> When you don't opt in to mutability
Rust's whole shtick is safe mutability.
You cut off the part where I said sum AND product types. Variant and union are post C++11 and not traditionally part of Algol style languages. Even nowadays the only time I see variant is from some haskell engineer stuck on a C++ job.
>Pattern matching is not wide spread, but --while nice to have-- it's syntactic sugar.
No pattern matching is a safety feature. Most of rust code should be using pattern matching as much as possible. In fact there's a language called Elm that achieves a claim for code that has ZERO runtime exceptions via the pattern matching feature. See: https://elm-lang.org/
Don't believe me? You know about programmings biggest mistake right? Null? Well C++ sort of got rid of it by discouraging it's use but even with optionals the issue is still there because optionals can still crash your program.
With rust, optionals never crash unless you deliberately ask it to via unwrap. If you use pattern matching exclusively to unroll optionals a crash is virtually impossible. Try it, and try to guess how pattern matching prevents optionals from crashing your program in rust, while in C++ the lack of pattern matching forces empty optionals to crash when you try to access it.
>Rust's whole shtick is safe mutability.
Yes, but at the same time it's opt in. Algol languages are by default mutable and you have to opt in for immutability via keywords like const. Rust offers extended safety via a single usage mutable borrow ans single usage mutable ownership but this is opt in. You should avoid using the keyword mut as much as possible.
It is very rare that anyone is actually using the truly advanced techniques like knot tying or TARDIS monads which are truly impossible in other languages.
let samples: Vec<_> = iterator.collect();
The RHS by itself has the type "B for any B that implements FromIterator<I> where I is the Item type of the given iterator". This cannot be assigned directly to a variable without any type hint (like with C++ auto) because it's an entire class of types and not a specific type. However, in the provided example, the Rust type checker can use the provided clue to narrow this down to an exact type, Vec<I>, without requiring an explicit statement of the type I (instead the underscore serves as a placeholder). std::vector v2 (v1.begin() + 1, v1.begin() + 2);
> However, in the provided example, the Rust type checker can use the provided clue to narrow this down to an exact typeThe Rust example is incomplete and its RHS by the time it gets to `.collect()` within the context of `iterator` has to be bound to a particular type of the context via `impl Iterator for <MyContext>`. This is pretty much the same thing as template argument deduction for class templates in C++17 [1][2], and in C++20 it got extended to generic concepts and constraints [3]:
#include <numeric>
#include <vector>
#include <iostream>
#include <concepts>
template <typename T>
requires std::integral<T> || std::floating_point<T>
constexpr auto avg(std::vector<T> const &v) {
auto sum = std::accumulate(v.begin(), v.end(), 0.0);
return sum / v.size();
}
int main() {
std::vector v { 1, 2, 3 };
std::cout << avg(v) << std::endl;
}
Note that nowhere in the snippet do I specify the type explicitly except for the container of type vector.[1] https://en.cppreference.com/w/cpp/language/template_argument...
[2] https://devblogs.microsoft.com/cppblog/how-to-use-class-temp...
I came to Rust from Haskell (among other languages). I was a little confused by closures, but I attributed that to the fact that I started writing Rust before 1.0 when closures were far far far less convenient than they are today. (This is back when there were 'proc' closures.) Now I find them extremely natural.
Everything else pretty much falls into place.
The other thing that was added since you've looked at Rust is that you can return unboxed closures because Rust has gained the ability to express anonymous types directly by naming a trait it implements:
fn adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| y + x
}
fn main() {
let add2 = adder(2);
assert_eq!(7, add2(5));
assert_eq!(10, add2(8));
} fn adder(x: i32) -> impl Fn(i32) -> impl Fn(i32) -> i32 {
move |y| move |z| y + x + z
}
Alright, I can't. I can do that for a single closure, but can't for two or more of them. Here's why we have `#![feature(impl_trait_in_fn_trait_return)]` -- in Nightly, among many other such features. fn adder(x: i32) -> impl Fn(i32) -> Box<dyn Fn(i32) -> i32> {
move |y| Box::new(move |z| y + x + z)
}
fn main() {
let add23 = adder(2)(3);
assert_eq!(10, add23(5));
assert_eq!(13, add23(8));
}
It's almost like "fundamentally broken" (not just "broken" but "fundamentally broken") and "has subtlety" are two totally different things. What a fucking revelation.Now if you said, "Rust has some rough points at the intersection of generics, closures and async programming," I'd say that's absolutely true!
This might rather confirm my point, since engineers using a specific programming language quite often have a contorted picture of how code in other languages is written, as they become more and more acquainted with their main tool. If we compare Rust closures with those of ML languages, it becomes pretty clear how natural closures are in ML and tricky in Rust.
Not quite a sensation either to anybody who happened to use closures in a typical Rust code, which, apparently, happens to have quite a lot of generics and async!
> This might rather confirm my point
It might, but it doesn't, because I don't only use Rust. Notice also how you've moved the goalposts from "fundamentally broken" (sensationalist drivel) to "how natural closures are in ML and tricky in Rust" (a not unreasonable opinion with lots of room to disagree on the degree of "natural" and "tricky").
Generics and closures work just fine:
use std::ops::Add;
fn adder<T: Copy + Add<Output=T> + 'static>(x: T) -> impl Fn(T) -> Box<dyn Fn(T) -> T> {
move |y| Box::new(move |z| y + x + z)
}
fn main() {
let add23 = adder(2)(3);
assert_eq!(10, add23(5));
assert_eq!(13, add23(8));
}
You keep making sensationalist generalizations. It's trivial to demonstrate that closures and generics work together just fine, as I've done above. Are there subtleties and complexities and other things that can make certain cases or areas tricky? Absolutely. But that's not the same as "not working." runState do
value <- get
put counter+1
The problem comes when you also want to do IO with that state; if your code is in the IO monad it has no access to the State monad and can't call those methods. There are ways to work around that but monad transformers give you a StateT which can be applied to any base monad, including IO, giving you the capability of both.You set up your pipeline in terms of monads and then when you want to execute it you run it in IO. You can generalize IO to MonadIO or define an alias to some transformer pinned at IO. It adds a Computational overhead. Transformers are a solution to monad composition which isn’t naturally possible.
I haven't claimed that closures are not working, since well, they are working, but under a very limited set of circumstances. Again, I see nothing sensational, since the trickery of using closures has been discussed elsewhere.
[1] https://stackoverflow.com/questions/34814423/possible-to-def...
Of course it's generic. If you were to write the type out for the closure, it would be generic over 'T'. (Whether that type ever actually gets written out that way or not is a different matter.) As far as I can tell, what you're saying is that Rust doesn't support higher-rank polymorphism. Which is true (for types, but not for lifetimes). But that's not the same as "generics don't work with closures."
> I haven't claimed that closures are not working
You've said:
> Closures in Rust are fundamentally broken.
(which was completely unqualified and not to a "very limited set of circumstances")
and (emphasis mine)
> But I can basically demonstrate other language features that are not working with closures, such as generics