What features of Odin do you dislike?

So you have to understand that the reason @(require_results) is NOT the default is because Odin is tailoring more to C programmers who are used to this. Other languages may require you to do the _ = foo() approach then opt into the “allow results to be ignored” behaviour. However, because we are tailoring to C programmers, this will annoy them more than you realize to the point of them getting angry.

You might think it’s a nice way to prevent mistakes, but it’s a trade-off between “minimizing mistakes” (which is still an empirical statement by the way) and “annoying C programmers too much”.

4 Likes

This is fairly minor, subjective and a nit pick but I wish Odin also supported while keyword which does nothing more than for { } or while could just be an alias for for.

May be it’s me personally, it always takes a bit of time to figure out for me mentally if it’s a unbounded for { } loop or a bounded for iteration loop.

2 Likes

You might think it’s a nice way to prevent mistakes, but it’s a trade-off between “minimizing mistakes” (which is still an empirical statement by the way) and “annoying C programmers too much”.

Surely there’s a better reason than not having to type a couple characters in exchange with catching the following, not to mention we could always introduce a flag/file-scoped option to make it more lax:

package main
import "core:fmt"

main :: proc() {
  i: int
  i_init(&i)
  // Whoops, we didn't set the value!
  fmt.println(i)
  // User's notified return values must be handled.
  // If this is not wanted, it's at this time the user may correct it with
  // `@(optional_last_result)` (explained below) or `@(optional_results)`.
  i_init_fixed(&i)
}

Error :: enum { Out_Of_Memory }
i_init :: proc(i: ^int) -> Error {
  if true { return Error.Out_Of_Memory }
  i^ = 12
  return nil
}
@(require_results) i_init_fixed :: proc(i: ^int) -> Error {
  if true { return Error.Out_Of_Memory }
  i^ = 12
  return nil
}

I tested several (imperative) languages to see where they were at, and Zig was the only one that actually caught this without a lint, so… I’m assuming I’m missing something at this point, but no one has articulated what it is, and it’s an important feature of every linter I can think of.

Another good reason is that the amount of application code (outside of the index value in for ... in loops, assuming that counts) that’s actually okay with ignoring results is insanely small.
I do understand there’s more cases in the standard library itself.

Reversing @(require_results) and possibly replacing #optional_allocator_error with one that more generally ignores the right-most return value (@(optional_last_result) or something) allows for flexibility in case ignoring the last value specifically, is wanted.

At the end of the day, I’m fine with creating lints or forking the compiler, but yeah. Just one of those things that’s caused many headaches these last few decades, until linters were more commonly used to catch mistakes.

I’m certainly no language designer, which is why I’m interested in any knowledge I may have missed.

Appreciate it!

Just out of curiosity, I grepped core code for require_results usage.

// rg "\s*::\s*proc" --stats
Number of procs: 12609

Number of require_results: 3993

per file results for require_results. :

crypto/_aes/ct64/ct64.odin:2
crypto/_aes/ct64/ct64_keysched.odin:2
crypto/_aes/hw_intel/ghash.odin:5
crypto/_aes/hw_intel/hw_intel_keysched.odin:4
crypto/_edwards25519/edwards25519.odin:4
crypto/_edwards25519/edwards25519_scalar.odin:1
crypto/aead/aead.odin:1
crypto/aead/low_level.odin:1
crypto/aes/aes_gcm.odin:1
crypto/chacha20poly1305/chacha20poly1305.odin:1
crypto/ristretto255/ristretto255.odin:3
crypto/ristretto255/ristretto255_scalar.odin:2
debug/trace/trace.odin:3
encoding/cbor/cbor.odin:2
encoding/csv/reader.odin:10
fmt/fmt.odin:15
math/bits/bits.odin:77
math/ease/ease.odin:35
math/fixed/fixed.odin:11
math/linalg/extended.odin:65
math/linalg/general.odin:81
math/linalg/glsl/linalg_glsl.odin:541
math/linalg/glsl/linalg_glsl_math.odin:52
math/linalg/hlsl/linalg_hlsl.odin:702
math/linalg/hlsl/linalg_hlsl_math.odin:58
math/linalg/specific.odin:188
math/linalg/specific_euler_angles_f16.odin:100
math/linalg/specific_euler_angles_f32.odin:100
math/linalg/specific_euler_angles_f64.odin:100
math/linalg/swizzle.odin:32
math/math.odin:369
math/math_basic.odin:42
math/math_basic_js.odin:27
math/math_erf.odin:14
math/math_gamma.odin:10
math/math_lgamma.odin:10
math/math_log1p.odin:9
math/noise/opensimplex2.odin:9
math/rand/distributions.odin:26
math/rand/exp.odin:1
math/rand/normal.odin:1
math/rand/rand.odin:19
mem/alloc.odin:23
mem/allocators.odin:53
mem/mem.odin:28
mem/mutex_allocator.odin:1
mem/rollback_stack_allocator.odin:15
mem/tlsf/tlsf.odin:3
mem/tlsf/tlsf_internal.odin:36
mem/tracking_allocator.odin:1
mem/virtual/arena.odin:10
mem/virtual/arena_util.odin:5
mem/virtual/virtual.odin:6
odin/parser/file_tags.odin:4
os/dir_unix.odin:1
os/dir_windows.odin:2
os/env_windows.odin:3
os/errors.odin:5
os/os.odin:7
os/os2/allocators.odin:3
os/os2/dir.odin:6
os/os2/dir_linux.odin:2
os/os2/dir_posix.odin:2
os/os2/dir_windows.odin:2
os/os2/env.odin:3
os/os2/errors.odin:2
os/os2/file.odin:9
os/os2/file_linux.odin:1
os/os2/file_util.odin:3
os/os2/file_windows.odin:5
os/os2/heap.odin:2
os/os2/internal_util.odin:9
os/os2/path.odin:2
os/os2/path_windows.odin:3
os/os2/pipe.odin:2
os/os2/pipe_linux.odin:1
os/os2/pipe_posix.odin:1
os/os2/pipe_windows.odin:1
os/os2/process.odin:16
os/os2/process_windows.odin:2
os/os2/stat.odin:7
os/os2/temp_file.odin:3
os/os2/user.odin:3
os/os_darwin.odin:37
os/os_freebsd.odin:35
os/os_haiku.odin:16
os/os_js.odin:14
os/os_linux.odin:41
os/os_netbsd.odin:38
os/os_openbsd.odin:37
os/os_wasi.odin:7
os/os_windows.odin:26
os/stat_unix.odin:5
os/stat_windows.odin:15
reflect/iterator.odin:2
reflect/reflect.odin:61
reflect/types.odin:34
relative/relative.odin:4
simd/x86/abm.odin:4
simd/x86/adx.odin:6
simd/x86/aes.odin:6
simd/x86/pclmulqdq.odin:1
simd/x86/rdtsc.odin:2
simd/x86/sha.odin:7
simd/x86/sse.odin:88
simd/x86/sse2.odin:203
simd/x86/sse3.odin:11
simd/x86/sse41.odin:60
simd/x86/sse42.odin:19
simd/x86/ssse3.odin:16
slice/slice.odin:60
slice/sort.odin:7
sync/chan/chan.odin:23
sys/linux/sys.odin:6
sys/valgrind/callgrind.odin:1
sys/valgrind/helgrind.odin:1
sys/valgrind/memcheck.odin:1
sys/valgrind/valgrind.odin:1
text/regex/compiler/compiler.odin:2
text/regex/parser/parser.odin:1
text/regex/regex.odin:5
text/regex/tokenizer/tokenizer.odin:5
text/scanner/scanner.odin:16
unicode/letter.odin:38
unicode/utf8/grapheme.odin:2

Seems like the majority of core code doesn’t use require_results, even though in a lot of cases it makes sense to use it, especially for functions that can return an error.

2 Likes

I’m one of the people that gets annoyed by @require_results usage a lot, when it’s used on functions with side effects I often don’t care about the result and am just calling the function for the side effect.

IMO @require_results should only be added on functions without side effects.

1 Like

I actually mentioned just how nice it is for your case, because it’s something you’ll instantly be aware of and have an easy resolution to—use _ =, submit a pull request, log a bug, or if you don’t mind modifying the standard library while you wait:

// If this is not wanted, it’s at this time the user may correct it with
// @(optional_last_result) (explained below) or @(optional_results).

Unfortunately, the opposite is not true. I’d rather you be slightly annoyed than developers and/or clients wasting a bunch of time on a bug we could have prevented.

3 Likes

How many cases are there where a function returns but the return can just be ignored / is not important? I would say the vast majority of times if a function is returning something, the return value is meant to be used. And the default should reflect that. Currently, you have to think about opting in to this feature which means that in a lot of cases where require_results makes sense don’t get annotated as such because it never occurred to the author of the code.

3 Likes

Biggest issues i have is the bindings, but these are sort of non solvable.
Like binding things like mbedTLS is painful one could use a tool todo it but it really doesn’t work for a lot of cases and creates awful code so you end up going down the rabbit hole of porting it all yourself.

I think though that this i still the better way than doing it like Zig where it basically has to ship an entire C Preproc and Lexer/Parser in order to natively add headers.

Another thing is the missing meta programming, which adds a lot of clutter to code mostly because the code i write at work is sort of like check all pointers to be not null, fall in range etc. with a meta program i could add all of this. I dunno about the state of core:odin and if that somehow could be used to add this stuff. But i also understand that the language should be more C like and there it will take quite some time till they get anything like that in C++ it will be there probably in C++29.
Also a reason why i’d like to have this option is to generate code from specification files i can basically already do that as of a sort of preprocessor but it sort of means maintaining a extra compiler just todo this translation. Its like Qmoc.

I haven’t used Odin extensively, but am starting to use it more for personal projects.

One thing out of the gate that bothered me is a relatively minor thing, but it’s something that I’ve seen more and more languages using, but don’t understand why it’s being used…

: and ->. I don’t see how/why they are necessary for syntax parsing. Go did it without them, and from the reading and watching I’ve done, my understanding is Odin took a lot of it’s syntax from Go and Jai.

I also understand that it’s baked into the language now, and there isn’t anything that can be done. And perhaps there is some nuance I am not seeing in language design/parsing that I don’t know because I haven’t tried to do it myself before. But I don’t see how or why they are necessary. Outside of readability (which a syntax highlighter will do a good job of helping with), I can’t see what it gains.

1 Like

Go doesn’t need : because it has qualifier keywords (var x int, type y int, const z int = ..., etc) AND the := symbol which is NOT like Odin’s (which is two separate symbols : and =). Odin’s declaration syntax is fundamentally just a different syntax and thus requires different rules. If Odin’s syntax was just x int, then that’s very difficult to parse quickly compared to x: int.

However, I am guessing you mean just in procedure parameters, and if so, then it’s for consistency. You are correct that you could parse the procedure parameters with : and -> but then it would be completely inconsistent with the rest of the language. You want and need consistency and coherency within a language.

As for ->, this exists to remove an ambiguity whilst parsing AND improve reading ability knowing where the return value is (if there is one).

3 Likes

Over the past few weeks, I’ve been porting the entirety of my C++ project to Odin. ~7000 lines of code ported thus far. My only complaint so far is that parametric polymorphism feels very underpowered. For example, the other day I was unable to write a very simple procedure like this:

spin :: proc(ctx: ^Context, value: ^$T, min_value: Maybe(T) = nil, max_value: Maybe(T) = nil, ... many more parameters ...) {
  // ...body...
}

This fails with a very vague error Cannot assign value 'nil' to Maybe($T) [...]. Using instead min_value := Maybe(T){} at least tells you that cannot use a polymorphic type for a compound literal. This seems to be related issue #4528? I’ve resigned myself to getting rid of default values in this proc.

I’m not convinced it would be much harder to parse. x int would be parsed, in my mind, as x, which is an unknown identifier (the parsing finishes because a space is hit). I need to look at the next token to determine the type. In this, it’s int.

A similar thing would still work for something like x, y int. The comma here would be a message that you aren’t expecting the type yet. The lack of a , after an identifier indicates we are now expecting a type. It could still be further expanded into x, y int, float as well, though not a huge fan doing that.

I also don’t necessarily agree with -> removing an ambiguity. The end of a procedure comes from ) not ->. The return value is defined between ) and {. I can understand it helping with readability.

Do you have an example where my (likely simplistic, ideal, and naive) idea on parsing here fails?

1 Like

Not having a way to get the types/function definitions of a module like a c header file.

I find difficult and bug prone having to define function types two times when i am making something with hot reloading when i could import only the types of the other module and defining like type_of(other_module.fn)

I find difficult and bug prone having to define function types two times

You don’t have to. I know what you are wanting, which is the old macro trick, but that has its own issues. In Odin, there is a way to enforce the type:

My_Proc :: proc(int) -> bool

foo : My_Proc : proc(x: int) -> bool {
    ...
}

As for not having a way to get the types and procedure definitions is kind of the point of NOT requiring header files. But this is kind of a tooling issue rather than a language one, and what you effectively want is a documentation generator similar to what we have at pkg.odin-lang.org for your own code. odin doc does work already and when the new release of Odin is out, it will be very fast to render too.

1 Like

Disclaimer, english is not my primary language.

Let me explain this better to see if i am not understanding or i am explaining it wrong.

I am starting from the idea that i can’t import a module if i don’t want to initialize any global variables
within that module

There is this one usecase that bugs me [1]:

I have two modules, lets say one is platform and the other is game
the platform is compiled as an executable and game as a dll

My hot reloading code requires a struct with the function definitions like

// game module
update :: proc(^Game_State) -> bool { /*[...]*/ }

// platform module
Lib :: struct {
  update : proc(rawptr) -> bool,
  start_game : proc(),
  deinit : proc(),
  // [...]
}

and that is fine, but here there is a problem, i can’t properly enforce the type because
the game module have the definitions

this is more apparent whith this snippet (i haven’t tested this part)

// game module
File_Info :: struct { /*[...]*/ }
platform_get_file_info_handle : proc(string) -> File_Info

// platform module
Lib :: struct {
  // [...]
  platform_get_file_info_handle: rawptr
}

Duplicated_File_Info :: struct { /*[...]*/ }
platform_get_file_info :: proc(file: string) -> Duplicated_File_Info { /*[...]*/ }

main :: proc() {
  // [...] when the lib is initialized

  // Edit: i think this is wrong and will not work, 
  // it would be necessary a `load_platform_functions` function in the game module
  // and call this function here, but it will complicate this example
  lib.platform_get_file_info_handle = platform_get_file_info
}

it is much easier to do something like

// game module
Get_File_Info_Fn :: proc(string) -> File_Info
platform_get_file_info_handle : Get_File_Info_Fn

// platform module

// if you hypothetically import only the type definitions of the game module
import "game"

// idk if type_of(game.platform_get_file_info_handle) works
platform_get_file_info : game.Get_File_Info_Fn :
  proc(file: string)
  -> game.File_Info { /*[...]*/ }

I know what you are wanting, which is the old macro trick

I think is this trick you are mentioning:

#define X_INPUT_SET_STATE(name) DWORD WINAPI name(DWORD dwUserIndex, XINPUT_VIBRATION* pVibration)
typedef X_INPUT_SET_STATE(x_input_set_state);
X_INPUT_SET_STATE(XInputSetStateStub) {
  return ERROR_DEVICE_NOT_CONNECTED;
}
static x_input_set_state *XInputSetState_ = XInputSetStateStub;
#define XInputSetState XInputSetState_

I like this, but i know it is not necessary in odin, and i like not having to do this

As for not having a way to get the types and procedure definitions is kind of the point of NOT requiring header files.

I didn’t understand this part

Again, i don’t know if i am not understanding or i am explaining it wrong.

[1] sorry that i only have one use case, but i didn’t want to forget to write this comment

Overall the language is excellent, and it’s my favorite systems language.

My biggest nit is the inability to split declaration and assignment when handling multiple return values. I’ve introduced bugs multiple times when declaring a unique error or failure value but then reading the default name.

So regarding that problem, this has other issues and a “solution” (which numerous trade-offs which are probably not worth it whatsoever) is this: Experimenting with variable reuse within declarations and its issues

Interesting proposal, I like it actually and would use it if added.

I’m curious if you’ve considered or tried something like this:

foo, if ok := bar(); !ok {
    // handle !ok
}
// use foo

it doesn’t shadow and it also doesn’t pollute the namespace with variations of error/success variables.

1 Like

Hell no. Did you not read that post?

I AM NOT LOOKING FOR SYNTAX SUGGESTIONS. PLEASE DO NOT GIVE ME THEM!

1 Like

Lol didn’t know you meant globally