How does one enforce type safety and readability using Julia


#1

I am currently designing a method that must be able to accept many different, but similar, encoders. The framework for this encoder is simple. It accepts a Vector of bytes and spits out a Vector of bytes.
How can I notate this kind of general behaviour in Julia?

Object-Oriented design does not work, because you can not define abstract types with functions like this

abstract Encoder
  function encode(Vector{UInt8});
end

type MySillyEncoder <: Encoder
  function encode(incomming::Vector{UInt8})
    incomming
  end

function doEncodig(encoder::Encoder, bytes::Vector{UInt8})
  encoder.encode(bytes)
end

Functional design does not work, because you can not define the arguments of a function-parameter like this

function mySillyEncodeFunction(bytes::Vector{UInt8})::{Vector{UInt8}
  bytes
end

function doEncodig(encode::Function{Vector{UInt8}, Vector{UInt8}}, bytes::Vector{UInt8})
  encode(bytes)
end

doEncodig(mySillyEncodeFunction, ...)

Now since I can not guarantee that the object or function provided is actually confirming to my input and output rules all I can do is hope that the caller is doing it right. But the problem here is that if the caller does not do it right, all he can do is read examples or try to find some documentation about what we expect his encoder to look like.

Not being able to enforce behaviour and deduct that behaviour from signatures will make a large codebase brittle and easily breakable and hard to understand.

I understand that I could do the functional design path without specifying the types of the encode-function and that it will crash once typemissmatch occurs, but that does not solve the problem of the reader of my doEncoding function will not know what the input and output-types of my encode function are. He will have to slough trough my code and
will have to understand, more likely guess, as to what the encode function should do, accept and return.

I could also sprinkle Asserts and checks everywhere to help the caller, but this seems like something the language should provide.

Please understand that that encode is just an example here for much more complex function.


#2

I think you are looking for interfaces/traits. Unfortunately this is currently not provided by the julia language itself, but if you google for it, you can find various ways to work around it. See for example How to find if a method is implemented for an interface? or https://github.com/mauro3/SimpleTraits.jl.

See also https://github.com/JuliaLang/julia/issues/6975.


#3

What I like to do is put the result into some sort of type, for each encoder. So, for example,

abstract Encoder

type SillyEncoder <: Encoder
vector::Vector{Int}

function SillyEncoder(v::Vector{Int})
new(v+1)
end
end

type AlsoSillyEncoder <:Encoder
vector::Vector{Int}

function AlsoSillyEncoder(v::Vector{Int})
new(v.*2)
end
end

And then have a wrapper function that takes the encoder name and the original vector as input, and then output the encoded vector in a type. So,

function encoder{T}(::Type{T}, input::Vector{Int})
T(input)
end

Then, you can just call the wrapper function like so,

encoder(SillyEncoder, input)

and the result always will be a subtype of Encoder with its vector field having the Vector{Int} type.