Array basics

Hi,

In Haskell when you declare a function you are expected to say before hand what you expect as input and output for instance [Integers] → Integer. How does one do this is Julia. I am no Haskell expert by any strech but I really like the discipline. Partly because i miss that my code is starting to get sloppy.
For instance:

b and e are inputs that look like this :source or :type. Its the annotation used by Metagraphs to denote different metadata parts of a node or vertix. Its is not a Symbol.

function get_metadata(g::AbstractMetaGraph, l::Array,b,e)
    meta_list = []
    for i in 1:nv(g)
        if isin(l,get_prop(g,i,b))
           push!(meta_list,get_prop(g,i,e))
        else
            nothing
        end
    end
    return meta_list
end
  1. How can I check the type julia has given this? And how, in this particular case can I specify it?
  2. The output is an array of elements that are infact always Integers. How do I specify that instead of getting any? Do I do that here or at the moment I add the metadata to a node?
    3)is there documentation or a book specific to julia, which shows the way how you structure this discipline inside of Julia?
julia> B
3-element Array{Any,1}:
  7
 20
 24

You are instantiating

meta_list = []

which will create an array of type Any:

julia> []
Any[]

If you know meta_list will only hold integers, you should instantiate accordingly:

meta_list = Int[]
3 Likes

thanks that is one problem down :slight_smile:

1 Like

Here you go

function myfunc(a::Float64,b::Int64)::Float64
    return a + b
end

But it’s much more fun when you do not tell Julia what the types are. Then you can do wonderful things like this

julia> function myfunc2(a,b)
           return a * b
       end
myfunc2 (generic function with 1 method)

julia> myfunc2("hello ","world")
"hello world"

julia> myfunc2(3,5)
15

julia> myfunc2([4,5,6],3)
3-element Array{Int64,1}:
 12
 15
 18

julia> myfunc2([1 2 3;4 5 6;7 8 9],[11 12 13;14 15 16; 17 18 19])
3×3 Array{Int64,2}:
  90   96  102
 216  231  246
 342  366  390
5 Likes

Great example @StevenSiew.

In julia you do not need to annotate the output type as long as it can be inferred from the input types you are good, see this section on type stability. This way you do not need to write the same method over and over again.

I don’t think you should hardcode in the return type. It makes your code inflexible. Let Julia figure out the types, using, for example, an array comprehension:

meta_list = [get_prop(g, i, b) for i in 1:nv(g) if isin(l,get_prop(g,i,b))] 

Maybe try to figure out a way to avoid evaluating get_prop twice per iteration.

BTW, the else nothing clause has no effect whatsoever, and should be removed.

@StevenSiew

cool! but does speed not suffer due to none instantiation?

@DNF

Maybe try to figure out a way to avoid evaluating get_prop twice per iteration.

I get your point will look into it.

BTW, the else nothing clause has no effect whatsoever, and should be removed.

no, I understand that it does not do anything but I prefer to make it explicit.

As long as your function is type stable, you have no reduction in speed.

1 Like

It can be done more generically as well. If you know that get_prop(g,i,b) returns the same type of value for every i, (in your case always an Int), you could do:

meta_list = typeof(get_prop(g,1,e))[]

that way the meta_list will contain the elements of the same type returned by get_prop always, even if in some other case they turn out to be floats or something else.

julia> get(x,i) = x[i]
get (generic function with 1 method)

julia> function f(x)
         list = typeof(get(x,1))[]
         push!(list,get(x,1))
         list
       end
f (generic function with 1 method)

julia> f([1,1])
1-element Array{Int64,1}:
 1

julia> f([1.0,2.0])
1-element Array{Float64,1}:
 1.0


@lmiq

now thats a neat trick. will use! thanks :slight_smile:

Julia doesn’t need argument/return type annotations for speed because the method isn’t the final product, per se. When a function is called, Julia finds the most suitable method for that call’s argument types (dispatch). Then, Julia compiles the method for that call’s argument types (specialization), if it hasn’t already. One method can (and often is) called for different argument types, so one method can have many specializations. If you really want to document that a particular set of argument types results in a particular return type, you can jot it down in a comment or docstring. Just use @code_warntype to make sure it really does.

Annotating the return type just adds a type conversion step, maybe. If you already wrote your method to return that type given the function call’s argument types, then that step is omitted by the compiler. It’s rarely used because as others have demonstrated, this really limits what the method can do. Instead, you might be able to reuse this code for [Floats] → Float, [Bunnies] → Bunny, etc.

2 Likes

You can also use InitialValues.jl together with BangBang.jl

using InitialValues
using BangBang

g(x) = x*x

function f(x)
    res = InitialValue(push!!)
    for el in x
        res = push!!(res, g(el))
    end

    return res
end

with the following result

julia> x1 = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> f(x1)
3-element Vector{Int64}:
 1
 4
 9

julia> x2 = ["a", "b", "c"]
3-element Vector{String}:
 "a"
 "b"
 "c"

julia> f(x2)
3-element Vector{String}:
 "aa"
 "bb"
 "cc"

Be warned though, that the usage of push!! adds small overhead which may be or may be not crucial for you.

What those package bring that typeof()[] and push! do not? For that simple case they just seem to be obscuring the code.

1 Like

Well, first of all it is another approach and it is good to know another approaches, isn’t it? Secondly, they are used in various packages like Transducers.jl and as a next step one can switch from loops to transducers and the problem of initial value will go away by itself, but it is still good to understand how it works under the hood.

And thirdly, typeof construction is making additional call, and depending on the nature of the problem it may be a bad thing.

Consider following example

function g(x) 
    sleep(5)
    x*x
end

function f1(x)
    res = InitialValue(push!!)
    for el in x
        if el == 2
            res = push!!(res, g(el))
        end
    end

    return res
end

function f2(x)
    list = typeof(g(x[1]))[]
    for el in x
        if el == 2
            push!(list, g(el))
        end
    end

    return list
end

julia> x1 = [1, 2, 3]
julia> @time f1(x1)
  5.015492 seconds (6.64 k allocations: 349.130 KiB, 0.21% compilation time)
julia> @time f2(x1)
 10.021660 seconds (6.21 k allocations: 325.627 KiB, 0.11% compilation time)

At fourth, push!! is making type expansion, so in the following example

function g(x)
    if x < 2
        return 1
    else
        return 2.5
    end
end

julia> f1(x1)
1-element Vector{Float64}:
 2.5

julia> f2(x1)
ERROR: InexactError: Int64(2.5)

typeof function errors out.

Fifth, beauty in the eye of the beholder, I do not find that push!! is obscuring :slight_smile:

2 Likes

Thanks! (I didn’t mean to complain at all, sorry if I gave that impression). I was really curious and by reading the the package page I could not find out what the package did that a straight approach does not.

Indeed, I thought that avoiding that initial call was one of the things, but that seemed at first glance not much to justify someone writing a package. The type expansion is something nice, indeed.

1 Like

Well, that’s up to you, but to me it’s confusing that it’s there. It’s easy to start wondering what its purpose is, and whether there is some non-obvious effect. I’ve never seen something like that before.

2 Likes

See also this post and the surrounding discussion: Why specify argument types and return types - #6 by stevengj

I’ve also proposed an addition to the manual, since this kind of question comes up a lot: add basic overview of when to use type declarations by stevengj · Pull Request #39812 · JuliaLang/julia · GitHub

6 Likes