2009-07-22 25 views
24

Tôi muốn biết những gì giả Scala tốt nhất của Groovy safe-dereference operator (?.), hoặc ít nhất một số lựa chọn thay thế gần?Giả lập Scala tốt nhất của nhà điều hành an toàn-dereference của Groovy (?.)?

Tôi đã discussed it breifly trên blog Daniel Spiewak 's, nhưng muốn mở nó lên để Stackoverflow ...

Vì lợi ích của thời gian của mọi người, đây là phản ứng của Daniel ban đầu, truy cập của tôi, và phản ứng thứ 2 của mình :

@Antony

Actually, I looked at doing that one first. Or rather, I was trying to replicate Ragenwald’s andand “operator” from Ruby land. The problem is, this is a bit difficult to do without proxies. Consider the following expression (using Ruby’s andand, but it’s the same with Groovy’s operator):

test.andand().doSomething()

I could create an implicit conversion from Any => some type implementing the andand() method, but that’s where the magic stops. Regardless of whether the value is null or not, the doSomething() method will still execute. Since it has to execute on some target in a type-safe manner, that would require the implementation of a bytecode proxy, which would be flaky and weird (problems with annotations, final methods, constructors, etc).

A better alternative is to go back to the source of inspiration for both andand as well as Groovy’s safe dereference operator: the monadic map operation. The following is some Scala syntax which uses Option to implement the pattern:

val something: Option[String] = … // presumably could be either Some(…) or None

val length = something.map(_.length)

After this, length either be Some(str.length) (where str is the String object contained within the Option), or None. This is exactly how the safe-dereferencing operator works, except it uses null rather than a type-safe monad.

As pointed out above, we could define an implicit conversion from some type T => Option[T] and then map in that fashion, but some types already have map defined, so it wouldn’t be very useful. Alternatively, I could implement something similar to map but with a separate name, but any way it is implemented, it will rely upon a higher-order function rather than a simple chained call. It seems to be just the nature of statically typed languages (if anyone has a way around this, feel free to correct me).

Daniel Spiewak Monday, July 7, 2008 at 1:42 pm

câu hỏi thứ 2 của tôi:

Thanks for the response Daniel regarding ?. I think I missed it! I think I understand what you’re proposing, but what about something like this, assuming you don’t have control over the sources:

company?.getContactPerson?.getContactDetails?.getAddress?.getCity 

Say it’s a java bean and you can’t go in and change the return values to Something[T] - what can we do there?

Antony Stubbs Tuesday, July 21, 2009 at 8:07 pm oh gosh - ok on re-read that’s where you’re proposing the implicit conversion from T to Option[T] right? But would you still be able to chain it together like that? You’d still need the map right? hmm….

var city = company.map(_.getContactPerson.map(_.getContactDetails.map(_.getAddress.map(_.getCity)))) 

?

Antony Stubbs Tuesday, July 21, 2009 at 8:10 pm

phản ứng thứ 2 của ông:

@Antony

We can’t really do much of anything in the case of company?.getContactPerson, etc… Even assuming this were valid Scala syntax, we would still need some way to prevent the later calls in the chain. This is not possible if we’re not using function values. Thus, something like map is really the only option.

An implicit conversion to Option wouldn’t be bad, but by making things implicit, we’re circumventing some of the protection of the type system. The best way to do this sort of thing is to use for-comprehensions in concert with Option. We can do map and flatMap, but it’s much nicer with magical syntax:

for { 
    c < - company 
    person <- c.getContactPerson 
    details <- person.getContactDetails 
    address <- details.getAddress 
    } yield address.getCity 

Daniel Spiewak Tuesday, July 21, 2009 at 9:28 pm

T.B. nếu Daniel đăng câu trả lời gốc của mình lên blog của mình làm câu trả lời, tôi sẽ chỉnh sửa câu hỏi để xóa chúng vì lợi ích của Hệ thống.

+1

Xem thêm http://stackoverflow.com/questions/1364361/how-to-write-a-proper-null-safe-coalescing-operator-in-scala –

Trả lời

8

Làm thế nào về điều này?

def ?[A](block: => A) = 
    try { block } catch { 
    case e: NullPointerException if e.getStackTrace()(2).getMethodName == "$qmark" => null 
    case e => throw e 
    } 

Sử dụng đoạn mã này chút, bạn có thể tới đích một cách an toàn và mã chính nó là khá ngắn gọn:

val a = ?(b.c.d.e) 

một == null nếu b hoặc bc hoặc BCD hoặc bcde là null, nếu không, a = = bcde

Tôi nghĩ rằng giá trị của toán tử an toàn-dereference bị giảm đi khi bạn đang sử dụng một ngôn ngữ như Scala có cơ sở như gọi theo tên và implicits.

ps: Tôi sửa đổi mã ở trên một chút theo một trong các nhận xét bên dưới để xử lý trường hợp khi NullPointerException là thực sự được ném vào bên trong hàm được gọi.

BTW, tôi nghĩ rằng bằng cách sử dụng chức năng dưới đây là một cách nhiều thành ngữ viết Scala:

def ??[A](block: => A): Option[A] = ?(block) match { 
    case a: A => Some(a) 
    case _ => None 
    } 

như vậy:

??(a.b.c.d) match { 
    case Some(result) => // do more things with result 
    case None => // handle "null" case 
    } 
+0

Tất nhiên! Đẹp! Nhưng một lần nữa tôi rất ấn tượng với Scala! Điều này thậm chí còn tốt hơn cả toán tử Groovy! Tôi tự hỏi nếu bạn có thể làm tương tự với các đóng cửa trong Groovy? Chắc chắn sẽ thêm cái này vào hộp công cụ! Để xuất hiện trong phần mở rộng Wicket-Scala sớm ... Cảm ơn! –

+0

Bản trình diễn: scala> def?[A] (khối: => A) = | thử {block} catch {case e: NullPointerException => null} $ qmark: [A] (=> A) Bất kỳ scala> trường hợp class Địa chỉ (thành phố: String) scala> case class Person (a: Address)) scala> Công ty trường hợp lớp (p: Person) scala> var a = Công ty (Person (Địa chỉ ("Hải quan st"))) một: Công ty = Công ty (Person (Địa chỉ (Hải st))) scala> var b = Công ty (Person (null)) b: Công ty = Công ty (Person (null)) scala> println (apacity) st Hải scala> println (b. p.a.city) java.lang.NullPointerException scala> println (? (b.p.a.city)) null –

+0

có lẽ bạn có thể chỉnh sửa nhận xét của mình và thêm bản trình diễn đó vào, nhưng với ngắt dòng? –

6

Ràng buộc đơn sắc (flatMap/map) với loại scala.Option. Hỗ trợ cũng được cung cấp bởi sự hiểu biết. Scalaz cung cấp một phong cách functor ứng dụng nếu bạn thích.

Điều này không tương đương, nhưng là giải pháp tốt hơn nhiều so với nhà điều hành của Groovy vì nhiều lý do.

+1

bạn có thể đưa ra ví dụ không? –

+0

Ví dụ: Một số (5) .map (_ + 5) == Một số (10) val foo: Tùy chọn [Int] = None foo.map (_ + 5) == Không –

+1

... và _how_ nó là một giải pháp tốt hơn _far_ một '? .'? –

12

Có hai điều cần được xem xét tại đây.

Trước tiên, có vấn đề về "không có gì". Làm thế nào để bạn chuỗi những thứ khi một phần của chuỗi có thể không trả lại bất cứ điều gì? Câu trả lời là sử dụng số liệu Optionfor. Ví dụ:

scala> case class Address(city: Option[String] = None, street: Option[String] = None, number: Option[Int] = None) 
defined class Address 

scala> case class Contact(name: String, phone: Option[String] = None, address: Option[Address] = None) 
defined class Contact 

scala> case class ContactDetails(phone: Option[String] = None, address: Option[Address] = None) 
defined class ContactDetails 

scala> case class Contact(phone: Option[String] = None, address: Option[Address] = None) 
defined class Contact 

scala> case class Person(name: String, contactDetails: Option[Contact] = None) 
defined class Person 

scala> case class Company(name: String, contactPerson: Option[Person] = None) 
defined class Company 

scala> val p1 = Company("ABC", Some(Person("Dean", Some(Contact(None, Some(Address(city = Some("New England")))))))) 
p1: Company = Company(ABC,Some(Person(Dean,Some(Contact(None,Some(Address(Some(New England),None,None))))))) 

scala> val p2 = Company("Finnicky", Some(Person("Gimli", None))) 
p2: Company = Company(Finnicky,Some(Person(Gimli,None))) 

scala> for(company <- List(p1, p2); 
    | contactPerson <- company.contactPerson; 
    | contactDetails <- contactPerson.contactDetails; 
    | address <- contactDetails.address; 
    | city <- address.city) yield city 
res28: List[String] = List(New England) 

Đây là cách bạn có nghĩa vụ viết mã có thể trả về cái gì đó hoặc không ở Scala.

Vấn đề thứ hai, tất nhiên, là đôi khi bạn có thể không có quyền truy cập vào mã nguồn để thực hiện chuyển đổi phù hợp. Trong trường hợp này, có một số chi phí cú pháp bổ sung được đầu, trừ khi có thể sử dụng ngầm định. Tôi sẽ đưa ra một ví dụ dưới đây, trong đó tôi sử dụng một hàm "toOption" - có một điều như vậy trên Scala 2.8, trong đó tôi sẽ nói về bên dưới.

scala> def toOption[T](t: T): Option[T] = if (t == null) None else Some(t) 
toOption: [T](t: T)Option[T] 

scala> case class Address(city: String = null, street: String = null, number: Int = 0) 
defined class Address 

scala> case class Contact(phone: String = null, address: Address = null) 
defined class Contact 

scala> case class Person(name: String, contactDetails: Contact = null) 
defined class Person 

scala> case class Company(name: String, contactPerson: Person = null) 
defined class Company 

scala> val p1 = Company("ABC", Person("Dean", Contact(null, Address(city = "New England")))) 
p1: Company = Company(ABC,Person(Dean,Contact(null,Address(New England,null,0)))) 

scala> val p2 = Company("Finnicky", Person("Gimli")) 
p2: Company = Company(Finnicky,Person(Gimli,null)) 

scala> for(company <- List(p1, p2); 
    | contactPerson <- toOption(company.contactPerson); 
    | contactDetails <- toOption(contactPerson.contactDetails); 
    | address <- toOption(contactDetails.address); 
    | city <- toOption(address.city)) yield city 
res30: List[String] = List(New England) 

Hãy nhớ rằng bạn có thể khá sáng tạo khi đặt tên hàm. Vì vậy, thay vì "toOption", tôi có thể đã đặt tên là "?", trong trường hợp này tôi sẽ viết những thứ như "?(address.city)".

Nhờ nuttycom để nhắc nhở tôi, trên Scala 2,8 có một nhà máy Option trên đối tượng Option, vì vậy tôi chỉ có thể viết Option(something). Thực tế, bạn có thể thay thế "toOption" ở trên bằng "Option". Và nếu bạn thích sử dụng ?, bạn chỉ có thể sử dụng import với đổi tên.

+0

Phương pháp bạn đang nghĩ đến trong phiên bản 2.8 là Option.apply(). Vì vậy, cũng giống như bạn có một số (a) và không, bây giờ bạn có tùy chọn (a) trả về nếu (a == null) Không có ai khác (a) –

+0

Cho 2,8, bạn thực sự có thể nhận được "?" đơn giản bằng cách nhập scala. {Option =>?} –

2

Bởi vì đây sẽ trông khủng khiếp như một lời nhận xét, đây là một nhận xét phiên bản của mã của Walter:

/** 
* Safe dereference operator. E.g. ?(a.b.c.null.dd) 
*/ 
def ?[A](block: => A) = { 
    try { block } catch { 
    // checks to see if the 3rd to last method called in the stack, is the ?() function, 
    // which means the null pointer exception was actually due to a null object, 
    // otherwise the ?() function would be further down the stack. 
    case e: NullPointerException if e.getStackTrace()(2).getMethodName == "$qmark" => {null} 
    // for any other NullPointerException, or otherwise, re-throw the exception. 
    case e => throw e 
    } 

Và đặc điểm kỹ thuật, thông qua:

case class Company(employee:Employee) 
case class Employee(address:Address){ 
    def lookupAddressFromDb:Address = throw new NullPointerException("db error") 
} 
case class Address(city:String) 

"NullSafe operater" should { 
    "return the leaf value when working with non-null tree" in { 
    val company = Company(Employee(Address("Auckland"))) 
    val result = ?(company.employee.address.city) 
    result mustEq "Auckland" 
    } 
    "return null when working with a null element at some point in the tree" in { 
    val company = Company(null) 
    val result = ?(company.employee.address.city) 
    result must beNull 
    } 
    "re-throw the NPE when working with a method which actually throws a NullPointerException" in { 
    val company = Company(Employee(Address("Auckland"))) 
    ?(company.employee.lookupAddressFromDb.city) aka "the null-safe lookup method" must throwA[NullPointerException] 
    } 
} 
4

Không bom mìn mà một đồng nghiệp

class NullCoalescer[T <: AnyRef](target: T) { 
    def ?? (other: T) = 
     if(target == null) other else target 
} 
object NullCoalescerConversions { 
    implicit def toNullCoalescer[T <: AnyRef](target: T): NullCoalescer[T] = 
     new NullCoalescer(target) 
} 

println (System.getProperty("maybe") ?? "definitely") 
4

Để theo dõi câu trả lời Daniel C. Sobral của, lý do lựa chọn được ưa thích là bởi vì thành ngữ Scala không sử dụng con trỏ null. Nếu bạn có thể, viết lại mã để trả về Tùy chọn thay vì tham chiếu nullable. Các sơ đồ khối có dạng được sạch sẽ hơn so với việc đọc hiểu vì bạn không cần một tên biến mới cho mỗi bước. Nếu tất cả các giá trị là không bắt buộc (như trong ví dụ Groovy), cách tiếp cận Scala sẽ trông như thế này:

(company flatMap _.getContactPerson 
     flatMap _.getContactDetails 
     flatMap _.getAddress 
     flatMap _.getCity) match { 
    case Some(city) => ... 
    case None  => ... 
} 

Nếu bạn phải sử dụng các giá trị nullable cho Java khả năng tương tác, đây là một cách tiếp cận cung cấp cho bạn an toàn mà không cần NPE-tranh cãi hay quá nhiều lộn xộn:

sealed trait Nullable[+A] { 
    def apply[B](f:A=>B): Nullable[B] 
} 

def ?[A](a: A) = a match { 
    case null => NullRef 
    case _ => Ref(a) 
} 

case class Ref[A](value: A) extends Nullable[A] { 
    def apply[B](f:A=>B) = ?(f(value)) 
} 

object NullRef extends Nullable[Nothing] { 
    def apply[B](f: Nothing=>B): Nullable[B] = NullRef 
} 


?(company)(_.getContactPerson)(_.getContactDetails)(_.getAddress)(_.getCity) match { 
    case Ref(city) => ... 
    case _   => ... 
} 

này nên dễ mở rộng sang một đơn nguyên Option-phong cách đầy đủ nếu muốn.

10

Tạo chuyển đổi ẩn này.

class SafeDereference[A](obj: A) { 
    def ?[B >: Null](function: A => B): B = if (obj == null) null else function(obj) 
} 

implicit def safeDereference[A](obj: A) = new SafeDereference(obj) 

Cách sử dụng không đẹp bằng Groovy nhưng không quá khủng khiếp.

case class Address(state: String) 
case class Person(first: String, last: String, address: Address) 
val me = Person("Craig", "Motlin", null) 

scala> me ? (_.first) 
res1: String = Craig 

scala> me ? (_.address) 
res2: Address = null 

scala> me ? (_.address) ? (_.state) 
res3: String = null 
0

Tôi thích việc sử dụng hiểu biết của Daniel C. Sobral --- nó đạt đến điểm nhanh hơn so với thác của lồng nhau match es tôi đã từng làm. Tuy nhiên, nó vẫn không thuận tiện lắm vì vẫn còn các biến giả trung gian (và quá nhiều đánh máy).

Chúng tôi muốn một cái gì đó như a?.b?.c?.d vì vậy chúng tôi không phải suy nghĩ về những gì đi vào giữa: chỉ cần cố gắng để có được một cái gì đó và cho tôi một Option trong trường hợp bạn không thể có được nó.

Đối với bối cảnh, giả sử tôi có

case class Inner(z: Option[Int]) 
case class Outer(y: Option[Inner]) 
val x = Some(Outer(Some(Inner(Some(123))))) 

mà tôi muốn giải nén. Việc hiểu sẽ giống như sau

for (tmp1 <- x; tmp2 <- tmp1.y; tmp3 <- tmp2.z) yield tmp3 

kết quả là Some(123). Vấn đề là quá nhiều biến tạm thời (và thực tế là nó đọc một phần lạc hậu).

Tôi tìm thấy nó dễ dàng hơn để làm điều đó với flatMap, như thế này

x.flatMap(_.y.flatMap(_.z)) 

hoặc

x flatMap {_.y flatMap {_.z}} 

đó cũng dẫn đến Some(123).

Người ta có thể giảm bớt tính cách rườm rà và sử dụng ? biểu tượng mong muốn bằng cách đưa ra một cách hiệu quả các loại Option một phương pháp ? rằng làm điều tương tự như flatMap. Option được niêm phong từ phân lớp, nhưng chúng tôi có thể mô phỏng phương pháp mới với chuyển đổi tiềm ẩn.

case class OptionWrapper[A](opt: Option[A]) { 
    def ?[B](f: (A) => Option[B]): Option[B] = opt.flatMap(f) 
} 
implicit def toOptionWrapper[T](opt: Option[T]) = OptionWrapper(opt) 
implicit def fromOptionWrapper[T](wrap: OptionWrapper[T]) = wrap.opt 

Và sau đó

x ? {_.y ? {_.z}} 

mang Some(123. Nó vẫn không hoàn hảo bởi vì có các dấu ngoặc lồng nhau và dấu gạch dưới mà bạn có để có được quyền, nhưng nó tốt hơn so với bất kỳ lựa chọn thay thế tôi đã nhìn thấy.

+0

Tôi chỉ nhận thấy rằng D. Gates có thể làm điều này mà không có dấu ngoặc nhọn, nhưng bàn điều khiển Scala của tôi nói 'lỗi: thiếu kiểu tham số cho hàm mở rộng ((x $ 1) => z.flatMap (x $ 1.y))' khi tôi cố gắng 'x flatMap _.y'. Tôi thường gặp phải những lỗi này và buộc phải thêm cú pháp trang trí hơn. –

Các vấn đề liên quan