wffl

vaguely burnt

  • it/its

I do stuff; pfp by spicymochi



prophet
@prophet

Consider any polymorphic function that returns an option, say, a function for safely indexing into a list.

index<A> : (List<A>, Int) -> Option<A>

This function looks up a value at a given index and returns it (possibly wrapped in a Some) or returns null/None if the index is out of range, so it should be correct with either definition of Option, right?

No it's not.

index<Option<Int>>([0, null, 2], 1)

This returns null, so we can conclude that index 1 is out of range for the list, i.e. the list has at most one element. Huh.

The issue with null unions like this is that the result type of this function is

Option<Option<Int>> 
= Int | null | null 
= Int | null

Without a Some tag, there is no way to differentiate between None and Some(None). Was the index really out of range or was there just a null at the index? Dynamically typed langauges run into this issue all the time.

The linked Dhall code might be a little verbose, but it is correct and I will take that over brevity any day of the week.



You must log in to comment.

in reply to @cactus's post:

Rust does cause me pain in the amount of wrapping and unwrapping every single thing has to undergo. Also the Result and Option types need to be dealt with differently causes me to suffer

Python’s Union and Optional types are really nice

thing is in rust you could just write let foo = Some(6_i8);
it's extremely rare that you actually need to specify types of anything and even if you do you can generally throw _ and .into() everywhere

i find optionals to be annoying most of the time in rust, but at the same time it feels like the more people try to avoid the issues of optionals while keeping the upsides, the closer it seems like we’re getting to trying to reinvent null. idk that’s just my very uneducated programmer experience tho

the problem with null isn't that it exists, it's that anything could be null and you always have to remember to check. if you have a real type system that lets you specify where things can or cannot be null, null is less of a problem

Yeah and the closer a type system gets to strongly specifying “this might be null” and “this can never be null” the more it feels to me like it’s trying to reinvent optional, type systems are a flat circle

Julia has arbitrary Union types and teh equivalent to Optional[T] would bee Union{T,Nothing}, andf it works teh same way, hehe

const Optional{T} = Union{T,Nothing}

function add_with_defaults(x::Optional{T}, y::Optional{T}) where T<:Number
    a::T = x === nothing ? zero(T) : x
    b::T = y === nothing ? zero(T) : y
    return a + b
end
add_with_defaults(x::Nothing, y::Nothing) = 0

I vaguely rememberf them having nominal optional types at one pointf but I am pretty sure they just scraped them, meowf.

yeah I figured that probably existed, will definitely fn foo(x: impl Into<Option<i8>>) next time I need to. unfortunately there is no impl From<&str> for Option<String> so you can only skip one of the two types of annoying boilerplate in that case

  • typescript: null | number where either null or number is valid
  • zig: ?i32 + implicit coercion from T to ?T
  • rust: Option<i32> + Some(T)

unions like typescript are really nice until you make a generic type and realise that to be safe you need to use "null | {some: T}" in case the user passed in a nullable type, or just hope the user doesn't. but you can't enforce a constraint on the type argument to say it's not allowed to be nullable so like.

every time I write null | {some: T}, I get sad and wish I had real optionals

rust Some(T) is usually not nice but nice for generic types and nested optionals

zig is kind of cool until you do ???T and then have to do @as(??T, @as(?T, null)) or something to set the innermost optional to null.

in reply to @prophet's post:

Since I was talking aboot Julia, it solves this particular problem wiff both a Nothing type andf a Missing type to representf a value that is not present in an array/matrix. So you'd hav a Union{T, Nothing, Missing} .

U can of course just hav a singleton type for each time u need to return one of these union types and this is completely solved, as long as u hav the foresight. Idk if it's this easy in python, but in julia you'd just do:

struct ValueNotFound end
function searchvalue(f::Function, A::AbstractArray{T})::Union{T,ValueNotFound} where T
    for x in A
        if f(x)
            return x
        end
    end
    return ValueNotFound()
end

let maybe_value = searchvalue(>(2), Int[1,2,3,4,5])
    if maybe_value isa Int
        value::Int = maybe_value
        println("first value greater than 2 was $value")
    else
        @error "oh no! value not found!"
    end
end

Another solutionf is when depending on returning a argument thaf might hav a union type, to return an extra value in a tuple instead of just appending to teh union.

function searchvalue(f::Function, A::AbstractArray{T})::Tuple{Union{T,Nothing},Bool} where T
    for x in A
        if f(x)
            return (x,true)
        end
    end
    return (nothing,false)
end

let (maybe_value,found) = searchvalue(>(2), Int[1,2,3,4,5])
    if found
        value::Int = maybe_value
        println("first value greater than 2 was $value")
    else
        @error "oh no! value not found!"
    end
end

Without a Some tag, there is no way to differentiate between None and Some(None). Was the index really out of range or was there just a null at the index? Dynamically typed langauges run into this issue all the time.

In practice, I've never found it to be an issue. If you're worried about accessing past the end of an array, use a safe getter with a default return value and then check for the default value. This is the same as matching on the Option.

In Clojure, this is nth (for sequences, generic collections can use get): (nth [1 2 nil] 2 ::default) returns nil while (nth [1 2 3] 10 ::default) returns ::default. In Python, you don't have a built-in method on lists, but there's the iterools recipe nth which works the same: nth([1, 2, 3], 3, "hecked") returns "hecked". In Ruby, there's Array.fetch: [1, 2, 3].fetch(3, "hecked") returns "hecked".

Simple and easy, and in Clojure's case, it's the idiomatic/best method for accessing by index so no one will balk at your code using it.

in reply to @wffl's post:

Not the same article, but since cohost doesn’t allow mentioning other accounts, I’m gonna co-opt this comment section mwahahaha

https://ihatereality.space/02-you-would-not-use-filter-map/

I’m pretty sure this filter_map/flat_map iterator equivalence is an outgrowth on how hacky the rust stdlib iterator interface is.

If you look at the types in Haskell, you will quickly see that these are very different functions:

flatMap :: (Monad m) => m a -> (a -> m b) -> m b
filterMap :: (Filterable f) => f a -> (a -> Maybe b) -> f b

In particular, all ms in flatMap (which is called (>>=) in Haskell) have to be the same. But if you apply that to flatMap, you get Maybe a -> (a -> Maybe b) -> Maybe b.

If you want to do it to e.g. lists, you need to specialize filterMap (which is called mapMaybe in the Haskell stdlib where it is specialized to lists). Then, inserting [] for f, you get [a] -> (a -> Maybe b) -> [b], which filters a list and potentially changes its content.

My takeaway from this is that flat_map is not really monadic in rust’s iterators, which is a side-effect of rust not having any higher-kinded type and trait support.

Edit: That’s also the underlying reason why Haskell has different abstractions for traversable and foldable structures (Traversable and Foldable), some libraries like whitherable which provide more classes (e.g. the above Filterable abstracting mapMaybe for filtering and mapping over structures), and even streaming frameworks like conduit which allow for side-effecting pipelines that do automatic resource management (i.e. closing file handles once the end of files are reached).

Edit2: „but all of these are for-loops” – yes.