Argument datatype dependant of previous arguments?
from Araozu@lemmy.world to programming_languages@programming.dev on 05 Aug 2023 11:42
https://lemmy.world/post/2697004

While working with a dynamically typed lang, I came across this:

hash(password, algorithm, algorithmOptions)

Where algorithm is a constant signaling which hashing algorithm to use, and algorithmOptions is a dict whose keys depend on algorithm.

So I thought, can we dictate that if a previous parameter has this value, then this parameter has to have this other value?

E.g.

enum HashAlgo {
    Bcrypt,
    Argon2,
}

type BcryptOptions = {
    Int optionA,
    Int optionB,
}

type Argon2Options = {
    String optionC,
    String optionD,
}


// Here I make this type "depend" on an argument of type HashAlgo
type HashOptions = [HashAlgo] => {
    HashAlgo::Bcrypt => BcryptOptions,
    HashAlgo::Argon2 => Argon2Options,
}

fun hash(
    String password,
    HashAlgo algorithm,
    // Here I use HashOptions, passing the prev. argument
    HashOptions[algorithm] options,
)

This way the compiler can ensure the correct dict is used, based on the value of algorithm

Does something like this exist? I now realize that it would be impossible to type check in compile time based on a runtime value, but if it was allowed only for constants? What do you think?

#programming_languages

threaded - newest

fkn@lemmy.world on 05 Aug 2023 11:51 next collapse

This is possible with c++ templates.

Crazazy@feddit.nl on 05 Aug 2023 20:00 next collapse

I think what you are referring to is “dependent types” They usually occur in more advanced functional programming languages like Idris or Agda but Zig also has them in a way

armchair_progamer@programming.dev on 05 Aug 2023 20:16 next collapse

Multiple ways you can do this. Most of these should also extend to multiple arguments, and although the constant is promoted to type level, you can pass it around nested functions as a type parameter.

With generics

In Java (personally I think this approach is best way to implement your specific example; also Kotlin, C#, and some others are similar):

interface HashAlgo<Options> {
    String hash(String password, Options options);
}

class Bcrypt implements HashAlgo<BcryptOptions> { ... }
class Argon2 implements HashAlgo<Argon2Options> { ... }
record BcryptOptions { ... }
record Argon2Options { ... }

In Haskell without GADTs (also Rust is similar):

class HashAlgo opts where
  hash :: String -> opts -> String

data BcryptOptions = BcryptOptions { ... }
data Argon2Options = Argon2Options { ... }

instance HashAlgo BcryptOptions where
  hash password BcryptOptions { .. } = ...

instance HashAlgo Argon2Options where
  hash password Argon2Options { .. } = ...

In C (with _Generic):

typedef struct { ... } bcrypt_options;
typedef struct { ... } argon2_options;

JustTesting@lemmy.hogru.ch on 08 Aug 2023 06:28 collapse

This would be trivial in python with something like

from typing import overload
from enum import Enum

@overload
def hash(password: str, algorithm: BCryptAlgorithm, options: BCryptOptions):
    ...

@overload
def hash(password: str, algorithm: Argon2Algorithm, options: Argon2Options):
    ...

def hash(password: str, algorithm, options):
    [...implementation...]

Of course it’s python, so at runtime it wouldn’t matter, but the static type checker would complain if you called hash with BCryptAlgorithm and Argon2Options. You could also have it return different types based on the arguments and then in call sites it’d know which type will be returned based on the type of the arguments. And only the last function has am implementation, the @overload ones are just type signatures.

It’s documented here.