Skip to content

Commit 1f96813

Browse files
General improvements
1 parent c49b383 commit 1f96813

File tree

1 file changed

+22
-23
lines changed

1 file changed

+22
-23
lines changed

README.md

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ This is what that'd look like in psuedocode-
3333
```c
3434
typedef struct typeclass_name_vtable
3535
{
36-
ReturnType (*func_name)(void* self, ...);
36+
ReturnType (*const func_name)(void* self, ...);
3737
... /* More "abilities" */
3838
} TypeclassName_vtable;
3939

@@ -47,7 +47,7 @@ The latter struct is called a **Typeclass Instance**.
4747

4848
A polymorphic function could then look like-
4949
```c
50-
ReturnType poly_foo(TypeclassName x)
50+
void poly_foo(TypeclassName x)
5151
{
5252
/* Use x's abilities here */
5353
x.tc->func_name(self);
@@ -72,20 +72,20 @@ TypeclassName T_to_TypeclassName(T* x)
7272
return (TypeclassName){.tc = &tc, .self = x};
7373
}
7474
```
75-
This is called the **Implementation Function**. As expected, it accepts a pointer to a concrete type (since it has to be assignable to `void*`), wraps it around its typeclass instance, and returns it. The **lifetime** of the returned struct is the *same as the lifetime of the data pointed to by the given pointer*.
75+
This is called the **Implementation Function**. As expected, it accepts a pointer to that concrete type (since it has to be assignable to `void*`), wraps it around its typeclass instance, and returns it. The **lifetime** of the returned struct is the *same as the lifetime of the data pointed to by the given pointer*.
7676
7777
In general, neither this typeclass struct, nor the typeclass functions should take ownership of the concrete type. Though this isn't a forced requirement, just a suggested one.
7878
7979
That's all there is to it! *This* is the typeclass pattern (or interface pattern). These typeclass structs can be used in libraries to have polymorphic functions. The correct functions will be dynamically dispatched to.
8080
81-
The code snippets above are all psuedocode for a general idea. This core idea is used to define and implement a [`Show`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Text-Show.html#t:Show) typeclass for an `enum` in [barebones.c](./barebones.c).
81+
**The code snippets above are all psuedocode for a general idea. This core idea is used to define and implement a [`Show`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Text-Show.html#t:Show) typeclass for an `enum` in [barebones.c](./barebones.c).**
8282
8383
# Combining multiple typeclasses/interfaces
8484
*Reference code: [combined-barebones.c](./combined-barebones.c)*
8585
8686
In real world code, you'll require types that implement multiple typeclasses/interfaces. You can encode that by having a struct containing the usual `self` member, and *multiple* vtables - each corresponding to a specific typeclass.
8787
88-
Consider 2 typeclasses- [`Show`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Text-Show.html#t:Show) and [`Num`](https://hackage.haskell.org/package/base-4.15.0.0/docs/GHC-Num.html), their vtable structs looks like-
88+
Consider 2 typeclasses- [`Show`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Text-Show.html#t:Show) and [`Enum`](https://hackage.haskell.org/package/base-4.15.0.0/docs/GHC-Enum.html), their vtable structs looks like-
8989
```c
9090
typedef struct
9191
{
@@ -107,7 +107,7 @@ typedef struct
107107
EnumTC const* enumtc;
108108
} ShowEnum;
109109
```
110-
Now, you have a typeclass instance - that requires multiple typeclass implementations. You'd wrap a type into this *combined typeclass instance* by obtaining the `Show` and `Enum` typeclass implementations *for that type* (by calling the implementation functions), extracting the vtables from those instances and putting them into this struct.
110+
Now, you have a typeclass instance - that requires *multiple typeclass implementations*. You'd wrap a type into this *combined typeclass instance* by obtaining the `Show` and `Enum` typeclass implementations *for that type* (by calling the implementation functions), extracting the vtables from those instances and putting them into this struct.
111111

112112
If you implemented `Show` for `int`, and named the *implementation function* `int_to_show`, implemented `Enum` for `int`, and named the *implementation function* `int_to_enum`, this whole process would look like-
113113
```c
@@ -121,7 +121,7 @@ You now have **full**, **type safe**, and **flexible** *polymorphism*. Usable *i
121121
# For a few macros more
122122
*Reference code: [barebones-macro.c](./barebones-macro.c)*
123123
124-
This pattern, as implemented with maximum transparency above - may seem rather unintuitive for the implementor. It's very easy for the implementor of a typeclass to make mistakes in the way showcased above. You can, instead, make a macro to generalize the implementation-
124+
This pattern, as implemented with maximum transparency above, may seem rather unintuitive for the implementor. It's very easy for the implementor of a typeclass to make mistakes in the way showcased above. You can, instead, make a macro to generalize the implementation-
125125
```c
126126
#define impl_TypeclassName(T, Name, func_name_f) \
127127
TypeclassName Name(T* x) \
@@ -150,7 +150,7 @@ You can now simplify the implementation for `T` above-
150150
impl_TypeclassName(T, T_to_TypeclassName, T_func_name)
151151
```
152152

153-
The code snippets above are all psuedocode for a general idea. This concept is used to define a [`Show`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Text-Show.html#t:Show) typeclass for an `enum` in [barebones-macro.c](./barebones-macro.c).
153+
**The code snippets above are all psuedocode for a general idea. This concept is used to define a [`Show`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Text-Show.html#t:Show) typeclass for an `enum` in [barebones-macro.c](./barebones-macro.c).**
154154

155155
# With more constraints
156156
**NOTE**: The ideas and abstractions described in this section **are not** *integral* enough to the actual pattern. If you want maximum transparency, you may safely ignore this.
@@ -195,7 +195,7 @@ int main(void)
195195
}
196196
```
197197
198-
The code snippets above are all psuedocode for a general idea. This concept is used to define a [`Show`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Text-Show.html#t:Show) typeclass for an `enum` in [name-constrained.c](./name-constrained.c).
198+
**The code snippets above are all psuedocode for a general idea. This concept is used to define a [`Show`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Text-Show.html#t:Show) typeclass for an `enum` in [name-constrained.c](./name-constrained.c).**
199199
200200
# Here be meta-macros
201201
**NOTE**: The ideas and abstractions described in this section **are not** *integral* enough to the actual pattern. If you want maximum transparency, you may safely stop ignore this.
@@ -217,27 +217,26 @@ In general, defining all the typeclasses and its respective `impl_` macro is ver
217217
In general, you could have a singular macro to define the vtable and the typeclass instance together - it just needs to know some information about the functions. Next, you need a general `impl` macro. It should be able to deduce information about the functions of a typeclass (possibly through an object like macro), and define a function similar to how the current `impl_` macro does. You'll definitely need [`mapping`](https://github.com/swansontec/map-macro/blob/master/map.h) for this.
218218
219219
# Limitations
220-
* Polymorphic return types, i.e when the return type is a typeclass instance, generally involve heap allocation. This is because the `self` member is of type- `void*`. You can only assign pointers to it. But you can't assign the address of a local variable since its lifetime ends after the function returns.
221-
* There's no way to have a function's **return type**, be the *exact same* as a **polymorphic input (argument) type**. This is because there's no way to know *the exact type* wrapped inside a typeclass. You can return the same polymorphic type. But in many cases, this isn't what you'd want.
220+
1. Polymorphic return types, i.e when the return type is a typeclass instance, generally involve heap allocation. This is because the `self` member is of type- `void*`. You can only assign pointers to it. But you can't assign the address of a local variable since its lifetime ends after the function returns.
221+
2. There's no way to have a function's **return type**, be the *exact same* as a **polymorphic input (argument) type**. This is because there's no way to know *the exact type* wrapped inside a typeclass. You can return the same polymorphic type. But in many cases, this isn't what you'd want.
222222
223-
Consider addition- `(+) :: Num a => a -> a -> a` - 2 arguments and a return value, all of the same type. As long as the type implements `Num`. If you use `(+)` with `int`s, the return value is an `int`, with `float`s, the return value is a `float`.
223+
Consider addition- `(+) :: Num a => a -> a -> a` - 2 arguments and a return value, all of the same type. As long as the type implements `Num`. If you use `(+)` with `int`s, the return value is an `int`, with `float`s, the return value is a `float`.
224224
225-
You simply can't do this in C, since there's no way to capture those types. You could return the `Num` typeclass instance itself. But the only thing you can (safely) do with that return value, is more `Num` operations.
225+
You simply can't do this in C, since there's no way to capture those types. You could return the `Num` typeclass instance itself. But the only thing you can (safely) do with that return value, is more `Num` operations.
226226
227-
As an extension, you also can't have functions like `Enum a => toEnum :: Int -> a`.
228-
* [Return type polymorphism](https://eli.thegreenplace.net/2018/return-type-polymorphism-in-haskell/) (not to be confused with *polymorphic return types*) is simply not possible (safely).
229-
* It requires extra effort to pass [**combined typeclass instances**](#combining-multiple-typeclassesinterfaces) to functions expecting less typeclass implementations.
227+
3. As an extension to the point 2, [Return type polymorphism](https://eli.thegreenplace.net/2018/return-type-polymorphism-in-haskell/) (not to be confused with *polymorphic return types*) is simply not possible (safely). Which means functions like `Enum a => toEnum :: Int -> a` cannot be implemented.
228+
4. It requires extra effort to pass [**combined typeclass instances**](#combining-multiple-typeclassesinterfaces) to functions expecting less typeclass implementations.
230229
231-
Suppose you have the combined typeclass instance `Foo`. It contains the usual `self` member, and vtables for 3 other typeclasses `Atc`, `Btc`, `Ctc`. You want to use this with a function that just wants a type implementing `Atc` - the typeclass instance of which, is named `A`. You need to manually extract that `Atc` vtable from `Foo`, the `self` member, and then create the `A` struct by filling in `self` and `Atc`.
232-
* Type safety, on functions *taking multiple typeclass instance arguments*, but **requiring** those arguments to be backed up by **the same concrete type**, cannot be guaranteed.
230+
Suppose you have the combined typeclass instance `Foo`. It contains the usual `self` member, and vtables for 3 other typeclasses `Atc`, `Btc`, `Ctc`. You want to use this with a function that just wants a type implementing `Atc`. You need to manually extract the `Atc` vtable from `Foo`, the `self` member, and then create soley the `Atc` typeclass instance to be able to use it with the aforementioned function.
231+
5. Type safety, on functions *taking multiple typeclass instance arguments*, but **requiring** those arguments to be backed up by **the same concrete type**, cannot be guaranteed.
233232
234-
Consider the compare function- `Ord a => compare :: a -> a -> Ordering` (assume `Ordering` is `typedef enum { LT, EQ, GT } Ordering;`) - 2 polymorphic arguments, *bounded by the `Ord` typeclass*, but **required** to be *the same concrete type*. It wouldn't make sense to compare an `int` with a `char*` - and yet both can be wrapped inside a `Ord` typeclass instance, as long as they implement it. So if you have 2 `Ord` typeclass instances, and you want to call `compare` from one of them, pass in `self` from both `Ord` instances - there's no guarantee that both instances are actually wrapping the same type.
233+
Consider the compare function- `Ord a => compare :: a -> a -> Ordering` (assume `Ordering` is `typedef enum { LT = -1, EQ = 0, GT = 1 } Ordering;`) - 2 polymorphic arguments, *bounded by the `Ord` typeclass*, but **required** to be *the same concrete type*. It wouldn't make sense to compare an `int` with a `char*` - and yet both can be wrapped inside a `Ord` typeclass instance, as long as they implement it. So if you have 2 `Ord` typeclass instances, and you want to call `compare` from one of them, pass in `self` from both `Ord` instances - there's no guarantee that both instances are actually wrapping the same type.
235234
236-
There's a solution to this though, but you must do it manually. Before calling `compare`, **verify equality** of the `tc` members of both `Ord` instances. If they're wrapping the same type - obtained from their respective implementation function - the typeclass address is the exact same.
235+
There's a solution to this though, but you must do it manually. Before calling `compare`, **verify equality** of the `tc` members of both `Ord` instances. If they're wrapping the same type - obtained from their respective implementation function - the typeclass address is the exact same.
237236
238237
# Motivation
239-
It's pretty common for people to ask for polymorphism after they've written enough C. Thankfully, there's no shortage of demonstrations of implementing polymorphism in C. Mostly OOP polymorphism really, not too difficult to implement. But I wasn't too keen on this pattern. It had over reliance on giving up type safety. It was based around objects and "inheritance", rather than actions - which, some people (like me) prefer.
238+
It's pretty common for people to ask for polymorphism after they've written enough C. Thankfully, there's no shortage of demonstrations, helper headers, and crazy cool meta macros for implementing OOP polymorphism in C. But I was looking for an action oriented pattern with as much type safety as possible.
240239
241-
Instead, I wanted something like **Haskell typeclasses**, or **Java/C# interfaces**, or **Rust/Scala traits**. A sort of "interface" that declares a bunch of functions with specific types, leaving in a polymorphic `self` or the like. The exact implementations, for which, a concrete type must fill. This allows you to make polymorphic actions that ask for a type implementing a certain "interface".
240+
Specifically, I wanted something like **Haskell typeclasses**, or **Java/C# interfaces**, or **Rust/Scala traits**. A sort of "interface" that declares a bunch of functions with specific types, leaving in a polymorphic `self` or the like. The exact implementations, for which, a concrete type must fill. This allows you to make polymorphic actions that ask for a type implementing a certain "interface".
242241
243-
The end result, after about a week of experimenting and refining, is this pattern. Over all the refinements and re-evaluations, I think the final result is an actually extensible and practical polymorphism pattern, one based around actions. It's not a perfect encoding of actual typeclasses (especially not the haskell ones - those are extremely high level). But it's probably as good as it gets.
242+
The end result, after about a week of experimenting and refining, is this pattern. Over many refinements and re-evaluations, I think the final result is an actually extensible and practical polymorphism pattern based around actions. It's not a perfect encoding of actual typeclasses (especially not the haskell ones - those are extremely high level). But it's probably as good as it gets.

0 commit comments

Comments
 (0)