Filtering a dictionary in julia
Asked Answered
B

2

6

I want to filter a dictionary using filter() function but I am having trouble with it. What I wish to accomplish is, to return the key for some condition of the value. However I am getting a method error

using Agents: AbstractAgent

# Define types
mutable struct Casualty <: AbstractAgent
    id::Int 
    ts::Int
    rescued::Bool 

    function Casualty(id,ts; rescued = false)
        new(id,ts,rescued)
    end
end

mutable struct Rescuer <: AbstractAgent
    id::Int
    actions::Int
    dist::Float64

    function Rescuer(id; action = rand(1:3) , dist = rand(1)[1])
        new(id,action,dist)
    end
end

cas1 = Casualty(1,2)
cas2 = Casualty(2,3)
resc1 = Rescuer(3)

agents = Dict(1=> cas1, 2 => cas2, 3 => resc1)

Now to filter

filter((k,v) -> v isa Casualty, agents)

# ERROR: MethodError: no method matching (::var"#22#23")(::Pair{Int64, AbstractAgent})

# what I truly wish to achieve is return the key for some condition of the value

filter((k,v) -> k ? v isa Casualty : "pass", agents)
# ofcourse I am not sure how to "pass" using this format

Any idea how I can achieve this. Thanks

Boylston answered 3/9, 2022 at 2:8 Comment(1)
Sidenote: instead of rand(1)[1] it is better to write just rand().Estebanesteem
G
8

For dictionaries filter gets a key-value pair, so do either (destructuring Pair):

julia> dict = Dict(1=>"a", 2=>"b", 3=>"c")
Dict{Int64, String} with 3 entries:
  2 => "b"
  3 => "c"
  1 => "a"

julia> filter(((k,v),) -> k == 1 || v == "c", dict)
Dict{Int64, String} with 2 entries:
  3 => "c"
  1 => "a"

or for example (getting Pair as a whole):

julia> filter(p -> first(p) == 1 || last(p) == "c", dict)
Dict{Int64, String} with 2 entries:
  3 => "c"
  1 => "a"

julia> filter(p -> p[1] == 1 || p[2] == "c", dict)
Dict{Int64, String} with 2 entries:
  3 => "c"
  1 => "a"

EDIT

Explanation why additional parentheses are needed:

julia> f = (x, y) -> (x, y)
#1 (generic function with 1 method)

julia> g = ((x, y),) -> (x, y)
#3 (generic function with 1 method)

julia> methods(f)
# 1 method for anonymous function "#1":
[1] (::var"#1#2")(x, y) in Main at REPL[1]:1

julia> methods(g)
# 1 method for anonymous function "#3":
[1] (::var"#3#4")(::Any) in Main at REPL[2]:1

julia> f(1, 2)
(1, 2)

julia> f((1, 2))
ERROR: MethodError: no method matching (::var"#1#2")(::Tuple{Int64, Int64})
Closest candidates are:
  (::var"#1#2")(::Any, ::Any) at REPL[1]:1

julia> g(1, 2)
ERROR: MethodError: no method matching (::var"#3#4")(::Int64, ::Int64)
Closest candidates are:
  (::var"#3#4")(::Any) at REPL[2]:1

julia> g((1, 2))
(1, 2)

As you can see f takes 2 positional argument, while g takes one positional argument that gets destructured (i.e. the assumption is that argument passed to g is iterable and has at least 2 elements). See also https://docs.julialang.org/en/v1/manual/functions/#Argument-destructuring.

Now comes the tricky part:

julia> h1((x, y)) = (x, y)
h1 (generic function with 1 method)

julia> methods(h1)
# 1 method for generic function "h1":
[1] h1(::Any) in Main at REPL[1]:1

julia> h2 = ((x, y)) -> (x, y)
#1 (generic function with 1 method)

julia> methods(h2)
# 1 method for anonymous function "#1":
[1] (::var"#1#2")(x, y) in Main at REPL[3]:1

In this example h1 is a named function. In this case it is enough to just wrap arguments in extra parentheses to get destructuring behavior. For anonymous functions, because of how Julia parser works an extra , is needed - if you omit it the extra parentheses are ignored.

Now let us check filter docstring:

filter(f, d::AbstractDict)

Return a copy of d, removing elements for which f is false. The function f is passed key=>value pairs.

As you can see from this docstring f is passed a single argument that is Pair. That is why you need to use either destructuring or define a single argument function and extract its elements inside the function.

Gynecocracy answered 3/9, 2022 at 6:3 Comment(3)
Slightly nicer syntax for the first version is filter((k, v)::Pair -> k == 1 || v == "c",, dict) (credit to Fredrik Ekre).Footle
I wonder why filter((k, v) -> ...) doesn't work? Why do there need additional parentheses?Wizard
I have added an edit to explain this.Robber
A
1

The right syntax is:

filter(((k,v),) -> v isa Casualty, agents)

which prints

julia> filter(((k,v),) -> v isa Casualty, agents)
Dict{Int64, AbstractAgent} with 2 entries:
  2 => Casualty(2, 3, false)
  1 => Casualty(1, 2, false)

About the problem of only getting involved keys... I have no idea beside:

julia> filter(((k,v),) -> v isa Casualty, agents) |> keys

which prints

julia> filter(((k,v),) -> v isa Casualty, agents) |> keys
KeySet for a Dict{Int64, AbstractAgent} with 2 entries. Keys:
  2
  1
Allophone answered 3/9, 2022 at 6:6 Comment(1)
If Dict used a Tuple the solution would be the same. The point is that filter passes one positional argument not two to the predicate.Robber

© 2022 - 2024 — McMap. All rights reserved.