แลมบ์ดาใน Scala คืออะไรและมีประโยชน์อย่างไร?


152

บางครั้งฉันสะดุดเข้ากับสัญกรณ์กึ่งลึกลับของ

def f[T](..) = new T[({type l[A]=SomeType[A,..]})#l] {..} 

ในโพสต์ในบล็อกของสกาล่าซึ่งให้ "เราใช้กลอุบายประเภทแลมบ์ดา" นั้น

ในขณะที่ฉันมีสัญชาตญาณบางอย่างเกี่ยวกับเรื่องนี้ (เราได้รับพารามิเตอร์ประเภทไม่ระบุชื่อAโดยไม่ต้องทำให้เสียความหมายกับมันได้หรือไม่) ฉันไม่พบแหล่งที่มาที่ชัดเจนที่อธิบายถึงสิ่งที่ประเภทแลมบ์ดาหลอกลวงและสิ่งที่เป็นประโยชน์ มันเป็นแค่น้ำตาลซินแทคติคหรือมันเปิดมิติใหม่บางอย่าง?


ดูยัง
Shelby Moore III

คำตอบ:


148

lambdas ประเภทมีความสำคัญค่อนข้างน้อยเมื่อคุณทำงานกับประเภทที่สูงกว่า

ลองพิจารณาตัวอย่างง่ายๆในการกำหนด monad สำหรับการฉายภาพที่เหมาะสมของทั้ง [A, B] ประเภทของ monad มีลักษณะดังนี้:

trait Monad[M[_]] {
  def point[A](a: A): M[A]
  def bind[A, B](m: M[A])(f: A => M[B]): M[B]
}

ตอนนี้ทั้งสองเป็นตัวสร้างประเภทของสองข้อโต้แย้ง แต่ในการใช้ Monad คุณจะต้องให้มันเป็นตัวสร้างประเภทของอาร์กิวเมนต์หนึ่ง วิธีแก้ปัญหานี้คือการใช้แลมบ์ดาชนิด:

class EitherMonad[A] extends Monad[({type λ[α] = Either[A, α]})#λ] {
  def point[B](b: B): Either[A, B]
  def bind[B, C](m: Either[A, B])(f: B => Either[A, C]): Either[A, C]
}

นี่คือตัวอย่างของ currying ในระบบ type - คุณได้ curried ประเภทของ Either เช่นเมื่อคุณต้องการสร้างอินสแตนซ์ของ EitherMonad คุณต้องระบุหนึ่งในประเภทนั้น แน่นอนว่าจะมีการจัดหาให้ในเวลาที่คุณกำหนดจุดหรือผูก

เคล็ดลับประเภทแลมบ์ดาใช้ประโยชน์จากความจริงที่ว่าบล็อกว่างเปล่าในตำแหน่งประเภทสร้างประเภทโครงสร้างที่ไม่ระบุชื่อ จากนั้นเราจะใช้ # ไวยากรณ์เพื่อรับสมาชิกประเภท

ในบางกรณีคุณอาจจำเป็นต้องใช้ lambdas ชนิดที่ซับซ้อนกว่าซึ่งเป็นความเจ็บปวดในการเขียนแบบอินไลน์ นี่คือตัวอย่างจากรหัสของฉันตั้งแต่วันนี้:

// types X and E are defined in an enclosing scope
private[iteratee] class FG[F[_[_], _], G[_]] {
  type FGA[A] = F[G, A]
  type IterateeM[A] = IterateeT[X, E, FGA, A] 
}

คลาสนี้มีอยู่เฉพาะเพื่อให้ฉันสามารถใช้ชื่อเช่น FG [F, G] #IterateeM เพื่ออ้างถึงประเภทของ IterateeT monad ที่เชี่ยวชาญในหม้อแปลงรุ่นที่สองของ monad ซึ่งเป็นรุ่นที่สามของ monad เมื่อคุณเริ่มสแต็คการสร้างประเภทนี้จำเป็นมาก แน่นอนว่าฉันไม่เคยยกตัวอย่าง FG มีเพียงแฮ็คที่ให้ฉันแสดงสิ่งที่ฉันต้องการในระบบพิมพ์


3
น่าสนใจที่จะทราบว่าHaskell ไม่สนับสนุน lambdas ระดับประเภทโดยตรงแม้ว่าแฮ็กเกอร์ประเภทใหม่ (เช่นไลบรารี TypeCompose) มีวิธีการเรียงลำดับของการหลีกเลี่ยง
Dan Burton

1
ฉันอยากรู้ว่าคุณกำหนดbindวิธีสำหรับEitherMonadชั้นเรียนของคุณอย่างไร :-) นอกเหนือจากนั้นถ้าฉันอาจส่ง Adriaan เป็นครั้งที่สองที่นี่คุณไม่ได้ใช้ประเภทที่สูงกว่าในตัวอย่างนั้น คุณกำลังอยู่ในแต่ไม่ได้อยู่ในFG EitherMonadแต่คุณกำลังใช้ก่อสร้างประเภท* => *ซึ่งมีชนิด คำสั่งนี้เป็นลำดับที่ 1 ซึ่งไม่ใช่ "สูงกว่า"
Daniel Spiewak

2
ฉันคิดว่าชนิดที่*เป็นคำสั่งที่ 1 แต่ในกรณีใด ๆ Monad (* => *) => *มีชนิด นอกจากนี้คุณจะทราบว่าฉันได้ระบุ "การฉายภาพที่ถูกต้องของEither[A, B]" - การใช้งานนั้นไม่สำคัญ (แต่เป็นการออกกำลังกายที่ดีถ้าคุณยังไม่เคยทำมาก่อน!)
Kris Nuttycombe

ฉันเดาว่าจุดของดาเนียลที่ไม่ได้โทรหาที่*=>*สูงกว่านั้นเป็นสิ่งที่สมเหตุสมผลโดยการเปรียบเทียบว่าเราไม่ได้เรียกฟังก์ชั่นธรรมดา (ที่แมปฟังก์ชันที่ไม่ใช่ฟังก์ชันที่ไม่ใช่ฟังก์ชันในคำอื่น ๆ ค่าธรรมดากับค่าธรรมดา)
jhegedus

1
หนังสือ TAPL ของ Pierce หน้า 442:Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
jhegedus

52

ประโยชน์ที่ได้รับนั้นเหมือนกันกับฟังก์ชั่นที่ไม่ระบุชื่อ

def inc(a: Int) = a + 1; List(1, 2, 3).map(inc)

List(1, 2, 3).map(a => a + 1)

ใช้งานที่เช่นกับ Scalaz 7. เราต้องการที่จะใช้Functorที่สามารถ map Tuple2ฟังก์ชั่นที่ผ่านองค์ประกอบที่สองใน

type IntTuple[+A]=(Int, A)
Functor[IntTuple].map((1, 2))(a => a + 1)) // (1, 3)

Functor[({type l[a] = (Int, a)})#l].map((1, 2))(a => a + 1)) // (1, 3)

Scalaz ให้การแปลงโดยนัยที่สามารถอนุมานอาร์กิวเมนต์ประเภทFunctorได้ดังนั้นเรามักจะหลีกเลี่ยงการเขียนทั้งหมดนี้ บรรทัดก่อนหน้านี้สามารถเขียนใหม่เป็น:

(1, 2).map(a => a + 1) // (1, 3)

หากคุณใช้ IntelliJ คุณสามารถเปิดใช้งานการตั้งค่า, สไตล์โค้ด, สกาล่า, การพับ, พิมพ์ Lambdas จากนั้นซ่อนส่วน crufty ของไวยากรณ์และนำเสนอที่น่าพอใจมากขึ้น:

Functor[[a]=(Int, a)].map((1, 2))(a => a + 1)) // (1, 3)

รุ่นอนาคตของ Scala อาจสนับสนุนไวยากรณ์ดังกล่าวโดยตรง


ตัวอย่างสุดท้ายนั้นดูดีจริงๆ ปลั๊กอินสกาล่า IntelliJ ย่อมยอดเยี่ยมมาก!
AndreasScheinert

1
ขอบคุณ! แลมบ์ดาอาจหายไปในตัวอย่างสุดท้าย นอกจากนี้ทำไมฟังก์ชั่น tuple จึงเลือกที่จะแปลงค่าสุดท้าย? มันคือการประชุม / การเริ่มต้นในทางปฏิบัติ?
Ron

1
ฉันใช้ nightlies สำหรับ Nika และฉันไม่มีตัวเลือกความคิดอธิบายไว้ ที่น่าสนใจคือมีการตรวจสอบสำหรับ "แลมบ์ดาประเภทที่ประยุกต์ได้ง่าย"
Randall Schulz

6
มันถูกย้ายไปที่การตั้งค่า -> ตัวแก้ไข -> การพับโค้ด
retronym

@retronym ฉันมีข้อผิดพลาดเมื่อพยายาม(1, 2).map(a => a + 1)ใน REPL: `<console>: 11: ข้อผิดพลาด: แผนที่ค่าไม่ใช่สมาชิกของ (Int, Int) (1, 2) .map (a => a + 1) ^ '
เควินเมเรดิ ธ

41

ในการใส่สิ่งต่าง ๆ ในบริบท: คำตอบนี้โพสต์ครั้งแรกในหัวข้ออื่น คุณเห็นที่นี่เพราะมีการรวมสองเธรด คำแถลงคำถามในหัวข้อดังกล่าวมีดังนี้:

วิธีแก้ไขคำจำกัดความประเภทนี้: บริสุทธิ์ [({type? [a] = (R, a)}) #?]?

อะไรคือสาเหตุของการใช้การก่อสร้างดังกล่าว?

Snipped มาจากไลบรารี scalaz:

trait Pure[P[_]] {
  def pure[A](a: => A): P[A]
}

object Pure {
  import Scalaz._
//...
  implicit def Tuple2Pure[R: Zero]: Pure[({type ?[a]=(R, a)})#?] = new Pure[({type ?[a]=(R, a)})#?] {
  def pure[A](a: => A) = (Ø, a)
  }

//...
}

ตอบ:

trait Pure[P[_]] {
  def pure[A](a: => A): P[A]
}

ขีดเส้นใต้หนึ่งในกล่องหลังจากPนั้นก็หมายความว่ามันเป็นตัวสร้างประเภทใช้เวลาหนึ่งประเภทและส่งกลับประเภทอื่น ตัวอย่างประเภทของตัวสร้างประเภทนี้: List, Option.

ให้ชนิดคอนกรีตและมันจะช่วยให้คุณอีกประเภทคอนกรีต ให้และมันจะช่วยให้คุณ เป็นต้นListIntList[Int]ListStringList[String]

ดังนั้นList, Optionได้รับการพิจารณาให้เป็นฟังก์ชั่นระดับประเภทของ arity 1. * -> *อย่างเป็นทางการเราพูดว่าพวกเขามีชนิด เครื่องหมายดอกจันแสดงถึงชนิด

ตอนนี้Tuple2[_, _]เป็นตัวสร้างชนิดที่มีชนิด(*, *) -> *เช่นคุณต้องให้มันเป็นสองประเภทเพื่อรับชนิดใหม่

เนื่องจากลายเซ็นไม่ตรงกับคุณไม่สามารถทดแทนสำหรับTuple2 Pสิ่งที่คุณต้องทำคือนำไปใช้ Tuple2กับข้อโต้แย้งข้อใดข้อหนึ่งของมันซึ่งจะให้ตัวสร้างประเภทกับ* -> *เราและเราสามารถทดแทนมันPได้

น่าเสียดายที่สกาล่าไม่มีไวยากรณ์พิเศษสำหรับการใช้งานตัวสร้างแบบบางส่วนและดังนั้นเราจึงต้องหันไปใช้ความน่าพิศวงที่เรียกว่าแลมบ์ดาชนิด (สิ่งที่คุณมีในตัวอย่างของคุณ) พวกมันถูกเรียกว่าเพราะมันคล้ายกับการแสดงออกแลมบ์ดาที่มีอยู่ในระดับคุณค่า

ตัวอย่างต่อไปนี้อาจช่วยได้:

// VALUE LEVEL

// foo has signature: (String, String) => String
scala> def foo(x: String, y: String): String = x + " " + y
foo: (x: String, y: String)String

// world wants a parameter of type String => String    
scala> def world(f: String => String): String = f("world")
world: (f: String => String)String

// So we use a lambda expression that partially applies foo on one parameter
// to yield a value of type String => String
scala> world(x => foo("hello", x))
res0: String = hello world


// TYPE LEVEL

// Foo has a kind (*, *) -> *
scala> type Foo[A, B] = Map[A, B]
defined type alias Foo

// World wants a parameter of kind * -> *
scala> type World[M[_]] = M[Int]
defined type alias World

// So we use a lambda lambda that partially applies Foo on one parameter
// to yield a type of kind * -> *
scala> type X[A] = World[({ type M[A] = Foo[String, A] })#M]
defined type alias X

// Test the equality of two types. (If this compiles, it means they're equal.)
scala> implicitly[X[Int] =:= Foo[String, Int]]
res2: =:=[X[Int],Foo[String,Int]] = <function1>

แก้ไข:

ระดับค่าเพิ่มเติมและแนวระดับประเภท

// VALUE LEVEL

// Instead of a lambda, you can define a named function beforehand...
scala> val g: String => String = x => foo("hello", x)
g: String => String = <function1>

// ...and use it.
scala> world(g)
res3: String = hello world

// TYPE LEVEL

// Same applies at type level too.
scala> type G[A] = Foo[String, A]
defined type alias G

scala> implicitly[X =:= Foo[String, Int]]
res5: =:=[X,Foo[String,Int]] = <function1>

scala> type T = World[G]
defined type alias T

scala> implicitly[T =:= Foo[String, Int]]
res6: =:=[T,Foo[String,Int]] = <function1>

ในกรณีที่คุณนำเสนอพารามิเตอร์ประเภทRเป็นท้องถิ่นเพื่อฟังก์ชั่นTuple2Pureและดังนั้นคุณไม่สามารถกำหนดเพียงtype PartialTuple2[A] = Tuple2[R, A]เพราะไม่มีสถานที่ที่คุณสามารถใส่คำพ้องความหมายว่า

เพื่อจัดการกับกรณีเช่นนี้ฉันใช้เคล็ดลับต่อไปนี้ที่ใช้ประโยชน์จากสมาชิกประเภท (หวังว่าตัวอย่างจะอธิบายได้ด้วยตนเอง)

scala> type Partial2[F[_, _], A] = {
     |   type Get[B] = F[A, B]
     | }
defined type alias Partial2

scala> implicit def Tuple2Pure[R]: Pure[Partial2[Tuple2, R]#Get] = sys.error("")
Tuple2Pure: [R]=> Pure[[B](R, B)]

0

type World[M[_]] = M[Int]สาเหตุที่สิ่งที่เราใส่เข้าไปAในX[A]นั้นimplicitly[X[A] =:= Foo[String,Int]]เป็นเรื่องจริงเสมอฉันเดา

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.