Go中复杂关键字典的单一性但不是Julia?

时间:2017-06-01 13:37:49

标签: dictionary struct types julia

在GO中,当我使用结构作为地图的键时,键是一个单一的。

例如,以下代码生成只包含一个键的地图:map [{x 1}:1]

package main

import (
    "fmt"
)

type MyT struct {
    A string
    B int
}

func main() {

    dic := make(map[MyT]int)

    for i := 1; i <= 10; i++ {
        dic[MyT{"x", 1}] = 1
    }

    fmt.Println(dic)
}

// result : map[{x 1}:1]

我试图在朱莉娅做同样的事情,我有一个奇怪的惊喜:

这个Julia代码,类似于GO代码,生成一个带有10个密钥的字典!

    type MyT
        A::String
        B::Int64
    end

    dic = Dict{MyT, Int64}()

    for i in 1:10
        dic[MyT("x", 1)] = 1
    end

    println(dic)
    # Dict(MyT("x",1)=>1,MyT("x",1)=>1,MyT("x",1)=>1,MyT("x",1)=>1,MyT("x",1)=>1,MyT("x",1)=>1,MyT("x",1)=>1,MyT("x",1)=>1,MyT("x",1)=>1,MyT("x",1)=>1)

    println(keys(dic))
    # MyT[MyT("x",1),MyT("x",1),MyT("x",1),MyT("x",1),MyT("x",1),MyT("x",1),MyT("x",1),MyT("x",1),MyT("x",1),MyT("x",1)]

那我做错了什么?

谢谢@DanGetz的解决方案! :

immutable MyT     # or struct MyT with julia > 0.6
    A::String
    B::Int64
end

dic = Dict{MyT, Int64}()

for i in 1:10
    dic[MyT("x", 1)] = 1
end

println(dic)         # Dict(MyT("x", 1)=>1)
println(keys(dic))   # MyT[MyT("x", 1)]

3 个答案:

答案 0 :(得分:8)

可变值在Julia中通过标识哈希,因为没有关于类型表示什么的额外知识,人们不能知道具有相同结构的两个值是否意味着相同或不同。如果在将值用作字典键之后改变值,则按值散列可变对象可能会特别成问题 - 当通过标识进行散列时这不是问题,因为即使修改了可变对象的标识也保持相同。另一方面,按值散列不可变对象是完全安全的 - 因为它们不能被变异,因此这是不可变类型的默认行为。在给定的示例中,如果您使MyT成为不可变的,您将自动获得您期望的行为:

immutable MyT # `struct MyT` in 0.6
    A::String
    B::Int64
end

dic = Dict{MyT, Int64}()

for i in 1:10
    dic[MyT("x", 1)] = 1
end

julia> dic
Dict{MyT,Int64} with 1 entry:
  MyT("x", 1) => 1

julia> keys(dic)
Base.KeyIterator for a Dict{MyT,Int64} with 1 entry. Keys:
  MyT("x", 1)

对于包含要用作哈希键的StringInt值的类型,不可变性可能是正确的选择。实际上,不变性通常是正确的选择,这就是为什么引入结构类型的关键字在不可变结构中变为0.6到struct,为可变结构变为mutable struct的原因 - 人们的原则将首先达到更短,更简单的名称,因此这应该是更好的默认选择 - 即不变性。

正如@ntdef编写的那样,您可以通过重载Base.hash函数来更改类型的散列行为。但是,他的定义在某些方面是不正确的(这可能是我们未能更加突出和彻底地记录这一点的错误):

  1. 您要重载的Base.hash方法签名是Base.hash(::T, ::UInt)
  2. Base.hash(::T, ::UInt)方法必须返回UInt值。
  3. 如果您超载Base.hash,您还应该重载Base.==以匹配。
  4. 所以这将是一个正确的方法来按值生成可变类型哈希(重新定义MyT所需的新Julia会话):

    type MyT # `mutable struct MyT` in 0.6
        A::String
        B::Int64
    end
    
    import Base: ==, hash
    
    ==(x::MyT, y::MyT) = x.A == y.A && x.B == y.B
    
    hash(x::MyT, h::UInt) = hash((MyT, x.A, x.B), h)
    
    dic = Dict{MyT, Int64}()
    
    for i in 1:10
        dic[MyT("x", 1)] = 1
    end
    

    julia> dic
    Dict{MyT,Int64} with 1 entry:
      MyT("x", 1) => 1
    
    julia> keys(dic)
    Base.KeyIterator for a Dict{MyT,Int64} with 1 entry. Keys:
      MyT("x", 1)
    

    手动执行这有点烦人,但是AutoHashEquals包会自动执行此操作,从中解脱出来。您需要做的就是在type定义前添加@auto_hash_equals宏:

    using AutoHashEquals
    
    @auto_hash_equals type MyT # `@auto_hash_equals mutable struct MyT` in 0.6
        A::String
        B::Int64
    end
    

    底线:

    • 如果您的类型应该具有基于值的相等性和散列,请认真考虑使其不可变。

    • 如果您的类型确实必须是可变的,那么请仔细考虑将其用作哈希键是否是个好主意。

    • 如果您确实需要使用可变类型作为具有基于值的相等性和散列语义的散列键,请使用AutoHashEquals包。

答案 1 :(得分:4)

你没有做错任何事。这些语言之间的区别在于它们在将结构用作map / Dict中的键时如何选择对结构进行哈希处理。在go中,结构通过而不是指针地址进行哈希处理。这允许程序员通过使用结构而不是地图来更容易地实现多维地图。有关详细信息,请参阅this blog post

在Go中再现Julia的行为

要在go中重现Julia的行为,请重新定义地图以使用指向MyT的指针作为键类型:

func main() {

    dic := make(map[MyT]int)
    pdic := make(map[*MyT]int)

    for i := 1; i <= 10; i++ {
        t := MyT{"x", 1}
        dic[t] = 1
        pdic[&t] = 1
    }

    fmt.Println(dic)
    fmt.Println(pdic)
}

这里,pdic使用指向MyT结构的指针作为其键类型。因为在循环中创建的每个MyT具有不同的内存地址,所以密钥将是不同的。这会产生输出:

map[{x 1}:1]
map[0x1040a140:1 0x1040a150:1 0x1040a160:1 0x1040a180:1 0x1040a1b0:1 0x1040a1c0:1 0x1040a130:1 0x1040a170:1 0x1040a190:1 0x1040a1a0:1]

您可以使用此on play.golang.org。与Julia(见下文)不同,实现地图类型的方式意味着您无法为用户定义的结构指定自定义散列函数。

在Julia中重现Go的行为

Julia使用函数Base.hash(::K, ::UInt)来散列其Dict{K,V}类型的密钥。虽然它在文档中没有明确说明,但默认的散列算法使用object_id的输出,如the source code中所示。要在Julia中重现go的行为,请为您的类型定义一个新的hash函数,用于散列结构的值:

Base.hash(t::MyT, h::Uint) = Base.hash((t.A, t.B), h)

请注意,您还应该以相同的方式定义==运算符,以保证hash(x)==hash(y)隐含isequal(x,y)as mentioned in the documentation

然而,让Julia像你的例子一样行动的最简单方法是将MyT重新定义为immutable。作为一个不可变类型,Julia将使MyT的值为object_id而不是immutable MyT A::String B::Int64 end dic = Dict{MyT, Int64}() for i in 1:10 dic[MyT("x", 1)] = 1 end dic[MyT("y", 2)] = 2 println(dic) # prints "Dict(MyT("y",2)=>2,MyT("x",1)=>1)" 。举个例子:

$boundary = md5( uniqid() . microtime() );
$to      =  $user;
$headers[] = 'From: my@email.com';
$headers[] = 'MIME-Version: 1.0';   
$headers[] = 'Content-Type: multipart/alternative; boundary="'.$boundary.'"\r\n';
$subject = "The subject";
$message = "--" . $boundary . "\r\n";
$message .= "Content-type: text/plain; charset=utf-8\r\n";
$message .= "Content-Transfer-Encoding: 8bit\r\n\r\n"; 
$message .= 'Simple Text Message line 1\r\nline 2\r\nline 3';
$message .= "\r\n\r\n--" . $boundary . "\r\n";
$message .= "Content-type: text/html; charset=utf-8\r\n";
$message .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$message .= "<h1>html message!</h1>";
$message .= "\r\n\r\n--" . $boundary . "--";
mail($to, $subject, $message, implode("\r\n", $headers))

答案 2 :(得分:2)

编辑:请参阅@ StefanKarpinski的回答。 Base.hash函数必须返回UInt才能使其成为有效的哈希值,因此我的示例不起作用。对于涉及递归的用户定义类型,也存在一些愚蠢。

您获得10个不同密钥的原因是由于Julia在确定dict的密钥时使用hash函数。在这种情况下,我猜测它正在使用内存中对象的地址作为字典的键。如果您想明确地将(A,B)作为唯一键,则需要覆盖特定类型的hash函数,如下所示:

Base.hash(x::MyT) = (x.A, x.B)

这将复制Go行为,只有Dict中的一个项目。

Here's the documentation to the hash function.

希望有所帮助!