โทรตามชื่อเทียบกับการโทรตามมูลค่าใน Scala ต้องการการชี้แจง


239

ตามที่ฉันเข้าใจใน Scala ฟังก์ชันอาจถูกเรียกใช้เช่นกัน

  • โดยค่าหรือ
  • โดยชื่อ

ตัวอย่างเช่นจากการประกาศดังต่อไปนี้เรารู้หรือไม่ว่าฟังก์ชันจะถูกเรียกใช้อย่างไร?

ประกาศ:

def  f (x:Int, y:Int) = x;

โทร

f (1,2)
f (23+55,5)
f (12+3, 44*11)

มีกฎอะไรบ้าง?

คำตอบ:


540

ตัวอย่างที่คุณให้ใช้เพียงการโทรตามค่าดังนั้นฉันจะให้ตัวอย่างใหม่ที่เรียบง่ายขึ้นและแสดงความแตกต่าง

ก่อนอื่นสมมติว่าเรามีฟังก์ชั่นที่มีผลข้างเคียง Intฟังก์ชั่นนี้จะพิมพ์อะไรบางอย่างออกมาแล้วส่งกลับ

def something() = {
  println("calling something")
  1 // return value
}

ตอนนี้เรากำลังจะกำหนดฟังก์ชันสองฟังก์ชันที่ยอมรับIntอาร์กิวเมนต์ที่เหมือนกันทุกประการยกเว้นว่าจะรับอาร์กิวเมนต์ในลักษณะ call-by-value ( x: Int) และอีกฟังก์ชันหนึ่งในลักษณะ call-by-name ( x: => Int)

def callByValue(x: Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

ตอนนี้เกิดอะไรขึ้นเมื่อเราเรียกพวกมันมาพร้อมกับฟังก์ชั่นลดผลกระทบ

scala> callByValue(something())
calling something
x1=1
x2=1

scala> callByName(something())
calling something
x1=1
calling something
x2=1

ดังนั้นคุณจะเห็นได้ว่าในเวอร์ชันการโทรตามค่าผลข้างเคียงของการเรียกฟังก์ชันที่ส่งผ่าน ( something()) เกิดขึ้นเพียงครั้งเดียวเท่านั้น อย่างไรก็ตามในเวอร์ชันการโทรตามชื่อผลข้างเคียงเกิดขึ้นสองครั้ง

นี่เป็นเพราะฟังก์ชั่นการโทรตามค่าคำนวณค่าของนิพจน์แบบพาส - อินก่อนเรียกใช้ฟังก์ชันดังนั้นจึงมีการเข้าถึงค่าเดียวกันทุกครั้ง ฟังก์ชั่นการโทรตามชื่อจะคำนวณค่าของนิพจน์ที่ส่งผ่านใหม่ทุกครั้งที่เข้าถึง


296
ฉันคิดเสมอว่าคำศัพท์นี้ทำให้สับสนโดยไม่จำเป็น ฟังก์ชั่นสามารถมีหลายพารามิเตอร์ที่แตกต่างกันในสถานะการโทรตามชื่อเทียบกับการโทรตามค่า ดังนั้นไม่ใช่ว่าฟังก์ชั่นนั้นจะเรียกตามชื่อหรือเรียกตามค่ามันก็คือพารามิเตอร์แต่ละตัวนั้นอาจจะเป็นpass -by-name หรือ pass-by-value นอกจากนี้ "การเรียกร้องโดยชื่อ" มีอะไรจะทำอย่างไรกับชื่อ => Intเป็นประเภทที่แตกต่างจากInt; มันเป็น "ฟังก์ชั่นของการขัดแย้งไม่มีที่จะสร้างInt" VS Intเพียง เมื่อคุณมีฟังก์ชั่นชั้นหนึ่งคุณไม่จำเป็นต้องประดิษฐ์คำศัพท์การโทรตามชื่อเพื่ออธิบายสิ่งนี้
Ben

2
@Ben ที่ช่วยตอบคำถามสองสามข้อขอบคุณ ฉันหวังว่าจะเขียนเพิ่มเติมอธิบายความหมายของการส่งผ่านชื่อนี้อย่างชัดเจน
Christopher Poile

3
@SelimOber หากข้อความf(2)ถูกคอมไพล์เป็นนิพจน์ประเภทIntการเรียกใช้โค้ดที่สร้างขึ้นfพร้อมด้วยอาร์กิวเมนต์2และผลลัพธ์คือค่าของนิพจน์ หากข้อความเดียวกันนั้นถูกคอมไพล์เป็นนิพจน์ชนิด=> Intแล้วรหัสที่สร้างขึ้นจะใช้การอ้างอิงถึง "code block" บางประเภทเป็นค่าของนิพจน์ ทั้งสองวิธีค่าประเภทนั้นสามารถถูกส่งผ่านไปยังฟังก์ชันที่ต้องการพารามิเตอร์ประเภทนั้น ฉันค่อนข้างมั่นใจว่าคุณสามารถทำได้ด้วยการกำหนดตัวแปรโดยไม่มีพารามิเตอร์ส่งผ่าน ดังนั้นชื่อหรือการโทรมีอะไรเกี่ยวข้องกับมัน
เบ็น

4
@Ben ดังนั้นถ้า=> Intเป็น "ฟังก์ชั่นที่ไม่มีข้อโต้แย้งที่สร้าง Int" มันแตกต่างกัน() => Intอย่างไร? Scala ดูเหมือนว่าจะปฏิบัติต่อสิ่งเหล่านี้แตกต่างกันไปตัวอย่างเช่น=> Intดูเหมือนจะไม่ทำงานเป็นประเภทของ a valเฉพาะกับประเภทของพารามิเตอร์
ทิมกู๊ดแมน

5
@ TimGoodman คุณพูดถูกมันซับซ้อนกว่าที่ฉันคิดไว้เล็กน้อย => Intคือความสะดวกสบายและมันไม่ได้ถูกนำมาใช้อย่างถูกต้องตามวัตถุฟังก์ชั่น (สมมุติว่าทำไมคุณไม่สามารถมีตัวแปรประเภท=> Intแม้ว่าจะไม่มีเหตุผลพื้นฐานว่าทำไมสิ่งนี้ถึงใช้งานไม่ได้) () => Intเป็นฟังก์ชั่นที่ไม่มีข้อโต้แย้งอย่างชัดเจนว่าจะส่งคืนIntซึ่งจะต้องมีการเรียกอย่างชัดเจนและสามารถส่งผ่านเป็นฟังก์ชั่น => Intคือการจัดเรียงของ "ร็อกซี่Int" และสิ่งเดียวที่คุณสามารถทำอะไรกับมันเป็นเรียกว่า (โดยปริยาย) Intที่จะได้รับ
Ben

51

นี่คือตัวอย่างจาก Martin Odersky:

def test (x:Int, y: Int)= x*x

เราต้องการตรวจสอบกลยุทธ์การประเมินและกำหนดว่ากลยุทธ์ใดจะเร็วกว่า (น้อยกว่าขั้นตอน) ในเงื่อนไขเหล่านี้:

test (2,3)

เรียกค่า: ทดสอบ (2,3) -> 2 * 2 -> 4
เรียกตามชื่อ: ทดสอบ (2,3) -> 2 * 2 -> 4
ผลลัพธ์ที่ได้จะมาถึงด้วยจำนวนขั้นตอนเท่ากัน

test (3+4,8)

เรียกค่า: ทดสอบ (7,8) -> 7 * 7 -> 49
เรียกชื่อ: (3 + 4) (3 + 4) -> 7 (3 + 4) -> 7 * 7 -> 49
ที่นี่โทร โดยค่าจะเร็วขึ้น

test (7,2*4)

เรียกค่า: ทดสอบ (7,8) -> 7 * 7 -> 49
โทรหาโดยชื่อ: 7 * 7 -> 49 ที่
นี่เรียกตามชื่อเร็วกว่า

test (3+4, 2*4) 

เรียกค่า: ทดสอบ (7,2 * 4) -> ทดสอบ (7, 8) -> 7 * 7 -> 49
ชื่อเรียก: (3 + 4) (3 + 4) -> 7 (3 + 4) -> 7 * 7 -> 49
ผลลัพธ์ถึงขั้นตอนเดียวกัน


1
ในตัวอย่างที่สามสำหรับ CBV ฉันคิดว่าคุณหมายถึงการทดสอบ (7,8) แทนการทดสอบ (7,14)
talonx

1
ตัวอย่างนำมาจาก Coursera หลักการในการเขียนโปรแกรมสกาล่า การบรรยาย 1.2 การโทรตามชื่อควรอ่านdef test (x:Int, y: => Int) = x * xหมายเหตุว่าพารามิเตอร์ y ไม่เคยถูกใช้
dr jerry

1
เป็นตัวอย่างที่ดี! นำมาจาก Coursera MOOC :)
alxsimo

นี่เป็นคำอธิบายที่ดีเกี่ยวกับความแตกต่าง แต่ไม่ได้ตอบคำถามที่ถูกถามนั่นคือคำถามใดในสองเรื่องนี้ที่เรียกว่า Scala
db1234

16

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

def f(x: => Int, y:Int) = x

วิธีนี้พารามิเตอร์xจะไม่ถูกประเมินจนกว่าจะถูกเรียกในฟังก์ชัน

โพสต์เล็ก ๆนี้ที่นี่อธิบายอย่างนี้เช่นกัน


10

ในการทำซ้ำจุดของเบ็นในความคิดเห็นข้างต้นฉันคิดว่าเป็นการดีที่สุดที่จะนึกถึง "call-by-name" เป็นเพียงน้ำตาลประโยค parser เพียงแค่ล้อมรอบนิพจน์ในฟังก์ชั่นที่ไม่ระบุชื่อเพื่อให้พวกเขาสามารถเรียกได้ในภายหลังเมื่อมีการใช้

มีผลบังคับใช้แทนการกำหนด

def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

และทำงาน:

scala> callByName(something())
calling something
x1=1
calling something
x2=1

คุณสามารถเขียน:

def callAlsoByName(x: () => Int) = {
  println("x1=" + x())
  println("x2=" + x())
}

และเรียกใช้ดังต่อไปนี้สำหรับเอฟเฟกต์เดียวกัน:

callAlsoByName(() => {something()})

calling something
x1=1
calling something
x2=1

ฉันคิดว่าคุณหมายถึง: <! - ภาษา: lang-scala -> def callAlsoByName (x: () => Int) = {println ("x1 =" + x ()) println ("x2 =" + x ( ))} จากนั้น: <! - ภาษา: lang-js -> callAlsoByName (() => something ()) ฉันไม่คิดว่าคุณต้องการวงเล็บปีกกาที่มีบางสิ่งบางอย่าง () ในการโทรครั้งสุดท้ายนี้ หมายเหตุ: ฉันพยายามที่จะแก้ไขคำตอบของคุณ แต่การแก้ไขของฉันถูกปฏิเสธโดยผู้ตรวจสอบโดยระบุว่าควรเป็นความคิดเห็นหรือคำตอบแยกต่างหากแทน
lambdista

เห็นได้ชัดว่าคุณไม่สามารถใช้การเน้นไวยากรณ์ในความคิดเห็นดังนั้นเพียงละเว้นส่วน "<! - language: lang-scala ->"! ฉันจะแก้ไขความคิดเห็นของตัวเอง แต่คุณสามารถทำได้ภายใน 5 นาที! :)
lambdista

1
ฉันเพิ่งวิ่งเข้าไปในนี้เช่นกัน มันเป็นความตกลงที่จะแนวคิดที่จะคิดเช่นนี้ แต่ differentiates สกาล่าระหว่างและ=> T () => Tฟังก์ชั่นที่ใช้ชนิดแรกเป็นพารามิเตอร์จะไม่ยอมรับที่สองสกาล่าเก็บข้อมูลเพียงพอในการเพิ่มความคิดเห็น@ScalaSignatureเพื่อให้เกิดข้อผิดพลาดในการคอมไพล์ครั้งนี้ bytecode สำหรับทั้งสอง=> Tและเป็นเหมือนกันและแม้ว่าเป็น() => T Function0ดูคำถามนี้สำหรับรายละเอียดเพิ่มเติม
vsnyc

6

ฉันจะพยายามอธิบายโดยกรณีการใช้งานที่ง่ายมากกว่าเพียงแค่ให้ตัวอย่าง

ลองนึกภาพคุณต้องการสร้าง"แอป nagger"ที่จะทำให้ Nag ทุกครั้งตั้งแต่ครั้งสุดท้ายที่คุณถูกจับ

ตรวจสอบการใช้งานต่อไปนี้:

object main  {

    def main(args: Array[String]) {

        def onTime(time: Long) {
            while(time != time) println("Time to Nag!")
            println("no nags for you!")
        }

        def onRealtime(time: => Long) {
            while(time != time) println("Realtime Nagging executed!")
        }

        onTime(System.nanoTime())
        onRealtime(System.nanoTime())
    }
}

ในการนำไปปฏิบัติข้างต้นผู้ที่มีเหตุมีผลจะใช้งานได้ก็ต่อเมื่อผ่านชื่อเหตุผลก็คือเมื่อผ่านตามตัวอักษรมันจะถูกนำมาใช้ใหม่และดังนั้นค่าจะไม่ถูกประเมินอีกครั้งในขณะที่เมื่อผ่านชื่อค่าจะได้รับการประเมินทุก เวลาที่เข้าถึงตัวแปร


4

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

กลไก call-by-name ผ่านบล็อกรหัสไปยัง callee และทุกครั้งที่ callee เข้าถึงพารามิเตอร์บล็อกรหัสจะถูกดำเนินการและค่าจะถูกคำนวณ

object Test {
def main(args: Array[String]) {
    delayed(time());
}

def time() = {
  println("Getting time in nano seconds")
  System.nanoTime
}
def delayed( t: => Long ) = {
  println("In delayed method")
  println("Param: " + t)
  t
}
}
 1. C: /> scalac Test.scala 
 2. การทดสอบแบบสกาล่า
 3. ในวิธีการล่าช้า
 4. ใช้เวลาเป็นนาโนวินาที
 5. พารามิเตอร์: 81303808765843
 6. รับเวลาเป็นนาโนวินาที

2

ตามที่ฉันสันนิษฐานว่าcall-by-valueฟังก์ชั่นที่กล่าวถึงข้างต้นส่งผ่านค่าของฟังก์ชัน ตามที่Martin Oderskyมันเป็นกลยุทธ์การประเมินผลตามด้วยสกาล่าที่มีบทบาทสำคัญในการประเมินฟังก์ชั่น call-by-nameแต่ทำให้มันง่ายที่จะ Higher-Order-Functionsเช่นผ่านฟังก์ชั่นเป็นอาร์กิวเมนต์กับวิธีการที่เรียกว่า เมื่อวิธีการเข้าถึงค่าของพารามิเตอร์ที่ส่งผ่านมันเรียกการดำเนินงานของฟังก์ชั่นที่ส่งผ่าน ดังต่อไปนี้:

ตามตัวอย่าง @dhg ให้สร้างวิธีแรกเป็น:

def something() = {
 println("calling something")
 1 // return value
}  

ฟังก์ชันนี้มีหนึ่งprintlnข้อความสั่งและส่งคืนค่าจำนวนเต็ม สร้างฟังก์ชั่นที่มีข้อโต้แย้งเป็นcall-by-name:

def callByName(x: => Int) = {
 println("x1=" + x)
 println("x2=" + x)
}

พารามิเตอร์ฟังก์ชั่นนี้จะกำหนดฟังก์ชั่นที่ไม่ระบุชื่อที่ได้กลับมาเป็นจำนวนเต็มหนึ่งค่า ในนี้xมีความหมายของฟังก์ชั่นที่ได้0ผ่านการขัดแย้ง แต่intค่าตอบแทนและsomethingฟังก์ชั่นของเรามีลายเซ็นเดียวกัน callByNameเมื่อเราเรียกใช้ฟังก์ชันที่เราผ่านฟังก์ชั่นเป็นอาร์กิวเมนต์ไป แต่ในกรณีของcall-by-valueมันเพียงส่งค่าจำนวนเต็มไปยังฟังก์ชัน เราเรียกฟังก์ชันดังต่อไปนี้:

scala> callByName(something())
 calling something
 x1=1
 calling something
 x2=1 

ในที่นี้เราsomethingใช้วิธีการที่เรียกว่าสองครั้งเพราะเมื่อเราเข้าถึงค่าของxในcallByNameวิธีการโทรไปยัง defintion ของsomethingวิธีการ


2

การโทรตามค่าเป็นกรณีการใช้งานทั่วไปตามที่อธิบายโดยคำตอบมากมายที่นี่ ..

Call-by-nameจะส่งรหัสบล็อกไปยังผู้โทรและทุกครั้งที่ผู้เรียกเข้าถึงพารามิเตอร์บล็อกโค้ดจะถูกดำเนินการและค่าจะถูกคำนวณ

ฉันจะพยายามสาธิตการโทรด้วยชื่อแบบง่าย ๆ ด้วยกรณีการใช้ด้านล่าง

ตัวอย่างที่ 1:

ตัวอย่าง / กรณีใช้งานอย่างง่ายของการโทรตามชื่ออยู่ด้านล่างฟังก์ชั่นซึ่งรับฟังก์ชั่นเป็นพารามิเตอร์และให้เวลาผ่านไป

 /**
   * Executes some code block and prints to stdout the 
time taken to execute   the block 
for interactive testing and debugging.
   */
  def time[T](f: => T): T = {
    val start = System.nanoTime()
    val ret = f
    val end = System.nanoTime()

    println(s"Time taken: ${(end - start) / 1000 / 1000} ms")

    ret
  }

ตัวอย่างที่ 2:

apache spark (with scala) ใช้การบันทึกโดยใช้การโทรตามชื่อดูLoggingลักษณะ ที่มันประเมินว่าขี้เกียจlog.isInfoEnabledหรือไม่จากวิธีการด้านล่าง

protected def logInfo(msg: => String) {
     if (log.isInfoEnabled) log.info(msg)
 }

2

ในCall by Valueค่าของนิพจน์จะถูกคำนวณล่วงหน้าในเวลาที่เรียกใช้ฟังก์ชันและค่าเฉพาะนั้นจะถูกส่งผ่านเป็นพารามิเตอร์ไปยังฟังก์ชันที่เกี่ยวข้อง ค่าเดียวกันนี้จะถูกใช้ตลอดการทำงาน

ในขณะที่Call by Nameนิพจน์จะถูกส่งเป็นพารามิเตอร์ไปยังฟังก์ชันและคำนวณเฉพาะภายในฟังก์ชันเมื่อใดก็ตามที่พารามิเตอร์นั้นเรียกว่า

ความแตกต่างระหว่าง Call by Name และ Call by Value ใน Scala สามารถเข้าใจได้ดีขึ้นด้วยตัวอย่างด้านล่าง:

ตัวอย่างโค้ด

object CallbyExample extends App {

  // function definition of call by value
  def CallbyValue(x: Long): Unit = {
    println("The current system time via CBV: " + x);
    println("The current system time via CBV " + x);
  }

  // function definition of call by name
  def CallbyName(x: => Long): Unit = {
    println("The current system time via CBN: " + x);
    println("The current system time via CBN: " + x);
  }

  // function call
  CallbyValue(System.nanoTime());
  println("\n")
  CallbyName(System.nanoTime());
}

เอาท์พุต

The current system time via CBV: 1153969332591521
The current system time via CBV 1153969332591521


The current system time via CBN: 1153969336749571
The current system time via CBN: 1153969336856589

ในตัวอย่างโค้ดข้างต้นสำหรับการเรียกใช้ฟังก์ชันCallbyValue (System.nanoTime ())เวลา nano ของระบบจะถูกคำนวณล่วงหน้าและค่าที่คำนวณล่วงหน้าได้ถูกส่งผ่านพารามิเตอร์ไปยังการเรียกใช้ฟังก์ชัน

แต่ในการเรียกใช้ฟังก์ชัน CallbyName (System.nanoTime ())นิพจน์ "System.nanoTime ())" จะถูกส่งผ่านเป็นพารามิเตอร์สำหรับการเรียกฟังก์ชันและค่าของนิพจน์นั้นจะถูกคำนวณเมื่อมีการใช้พารามิเตอร์ภายในฟังก์ชันนั้น .

สังเกตุนิยามฟังก์ชั่นของฟังก์ชัน CallbyName โดยที่มีสัญลักษณ์=>คั่นพารามิเตอร์xและประเภทข้อมูล สัญลักษณ์เฉพาะนั้นมีบ่งชี้ว่าฟังก์ชั่นการโทรตามประเภทชื่อ

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

หวังว่านี่จะช่วยได้!


2

นี่คือตัวอย่างด่วนที่ฉันเขียนขึ้นเพื่อช่วยเพื่อนร่วมงานของฉันที่กำลังเรียนหลักสูตรสกาล่า สิ่งที่ฉันคิดว่าน่าสนใจคือมาร์ตินไม่ได้ใช้คำตอบ && คำถามที่นำเสนอก่อนหน้านี้ในการบรรยายเป็นตัวอย่าง ไม่ว่าในกรณีใด ๆ ฉันหวังว่าสิ่งนี้จะช่วยได้

val start = Instant.now().toEpochMilli

val calc = (x: Boolean) => {
    Thread.sleep(3000)
    x
}


def callByValue(x: Boolean, y: Boolean): Boolean = {
    if (!x) x else y
}

def callByName(x: Boolean, y: => Boolean): Boolean = {
    if (!x) x else y
}

new Thread(() => {
    println("========================")
    println("Call by Value " + callByValue(false, calc(true)))
    println("Time " + (Instant.now().toEpochMilli - start) + "ms")
    println("========================")
}).start()


new Thread(() => {
    println("========================")
    println("Call by Name " + callByName(false, calc(true)))
    println("Time " + (Instant.now().toEpochMilli - start) + "ms")
    println("========================")
}).start()


Thread.sleep(5000)

ผลลัพธ์ของรหัสจะเป็นดังต่อไปนี้:

========================
Call by Name false
Time 64ms
========================
Call by Value false
Time 3068ms
========================

1

พารามิเตอร์มักจะผ่านค่าซึ่งหมายความว่าพวกเขาจะได้รับการประเมินก่อนที่จะถูกแทนที่ในร่างกายของฟังก์ชั่น

คุณสามารถบังคับให้พารามิเตอร์เรียกตามชื่อโดยใช้ลูกศรคู่เมื่อกำหนดฟังก์ชัน

// first parameter will be call by value, second call by name, using `=>`
def returnOne(x: Int, y: => Int): Int = 1

// to demonstrate the benefits of call by name, create an infinite recursion
def loop(x: Int): Int = loop(x)

// will return one, since `loop(2)` is passed by name so no evaluated
returnOne(2, loop(2))

// will not terminate, since loop(2) will evaluate. 
returnOne(loop(2), 2) // -> returnOne(loop(2), 2) -> returnOne(loop(2), 2) -> ... 

1

มีคำตอบที่น่าอัศจรรย์มากมายสำหรับคำถามนี้ในอินเทอร์เน็ต ฉันจะเขียนคำอธิบายและตัวอย่างมากมายที่รวบรวมไว้เกี่ยวกับหัวข้อในกรณีที่บางคนอาจพบว่ามีประโยชน์

บทนำ

เรียกค่าตาม (CBV)

โดยทั่วไปแล้วพารามิเตอร์ของฟังก์ชั่นคือพารามิเตอร์ที่เรียกตามค่า นั่นคือพารามิเตอร์จะถูกประเมินจากซ้ายไปขวาเพื่อกำหนดค่าก่อนที่จะประเมินฟังก์ชันเอง

def first(a: Int, b: Int): Int = a
first(3 + 4, 5 + 6) // will be reduced to first(7, 5 + 6), then first(7, 11), and then 7

โทรตามชื่อ (CBN)

แต่ถ้าเราต้องเขียนฟังก์ชั่นที่ยอมรับว่าเป็นนิพจน์ที่เราไม่ต้องประเมินจนกว่ามันจะถูกเรียกใช้ในฟังก์ชั่นของเรา สำหรับกรณีนี้ Scala นำเสนอพารามิเตอร์แบบเรียกตามชื่อ ความหมายพารามิเตอร์ถูกส่งผ่านไปยังฟังก์ชันตามที่เป็นอยู่และการประเมินค่าจะเกิดขึ้นหลังจากการทดแทน

def first1(a: Int, b: => Int): Int = a
first1(3 + 4, 5 + 6) // will be reduced to (3 + 4) and then to 7

กลไกการโทรตามชื่อส่งต่อบล็อครหัสไปยังการโทรและทุกครั้งที่การเรียกเข้าถึงพารามิเตอร์บล็อกรหัสจะถูกดำเนินการและค่าจะถูกคำนวณ ในตัวอย่างต่อไปนี้ล่าช้าพิมพ์ข้อความที่แสดงให้เห็นว่าวิธีการที่ได้รับการป้อน ถัดไปล่าช้าพิมพ์ข้อความที่มีค่า ในที่สุดผลตอบแทนล่าช้า 't':

 object Demo {
       def main(args: Array[String]) {
            delayed(time());
       }
    def time() = {
          println("Getting time in nano seconds")
          System.nanoTime
       }
       def delayed( t: => Long ) = {
          println("In delayed method")
          println("Param: " + t)
       }
    }

ในวิธีการที่ล่าช้า
รับเวลาเป็นวินาทีนาโน
พารามิเตอร์: 2027245119786400

ข้อดีและข้อเสียสำหรับแต่ละกรณี

CBN: + ยุติบ่อยยิ่งขึ้น * ตรวจสอบการเลิกจ้างด้านล่าง * + มีข้อได้เปรียบที่อาร์กิวเมนต์ของฟังก์ชั่นไม่ได้รับการประเมินถ้าพารามิเตอร์ที่เกี่ยวข้องไม่ได้ใช้ในการประเมินผลของฟังก์ชั่นของร่างกาย - ช้าลงมันสร้างคลาสเพิ่มเติม โหลดนานขึ้น) และใช้หน่วยความจำเพิ่มขึ้น

CBV: + มันมักจะมีประสิทธิภาพมากกว่าชี้แจงแทน CBN เพราะมันหลีกเลี่ยงการคำนวณซ้ำของการแสดงออกของข้อโต้แย้งที่เรียกชื่อตามรายละเอียดซ้ำ มันประเมินทุก ๆ อาร์กิวเมนต์ของฟังก์ชั่นเพียงครั้งเดียว + มันเล่นได้ดีกว่ามากด้วยเอฟเฟกต์คำสั่งและผลข้างเคียงเพราะคุณมักจะรู้ดีกว่ามากเมื่อการแสดงออกจะถูกประเมิน - อาจนำไปสู่การวนซ้ำระหว่างการประเมินค่าพารามิเตอร์ * ตรวจสอบการเลิกจ้างด้านล่าง *

จะทำอย่างไรถ้าการเลิกจ้างไม่รับประกัน

- หากการประเมิน CBV ของนิพจน์ e สิ้นสุดลงการประเมิน CBN ของ e จะสิ้นสุดลงด้วย - ทิศทางอื่นไม่เป็นความจริง

ตัวอย่างที่ไม่สิ้นสุด

def first(x:Int, y:Int)=x

พิจารณาการแสดงออกก่อน (1, วน)

CBN: แรก (1, ลูป) → 1 CBV: แรก (1, ลูป) →ลดอาร์กิวเมนต์ของนิพจน์นี้ เนื่องจากหนึ่งเป็นลูปจึงลดการขัดแย้งอย่างไม่สิ้นสุด มันไม่ยุติ

ความแตกต่างในพฤติกรรมของแต่ละกรณี

ลองกำหนดวิธีการทดสอบที่จะ

Def test(x:Int, y:Int) = x * x  //for call-by-value
Def test(x: => Int, y: => Int) = x * x  //for call-by-name

การทดสอบ Case1 (2,3)

test(2,3)2*24

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

การทดสอบ Case2 (3 + 4,8)

call-by-value: test(3+4,8) → test(7,8)7 * 749
call-by-name: (3+4)*(3+4)7 * (3+4)7 * 749

ในกรณีนี้การโทรตามค่าจะดำเนินการตามขั้นตอนน้อยลง

การทดสอบ Case3 (7, 2 * 4)

call-by-value: test(7, 2*4) → test(7,8)7 * 749
call-by-name: (7)*(7)49

เราหลีกเลี่ยงการคำนวณอาร์กิวเมนต์ที่สองโดยไม่จำเป็น

การทดสอบ Case4 (3 + 4, 2 * 4)

call-by-value: test(7, 2*4) → test(7,8)7 * 749
call-by-name: (3+4)*(3+4)7*(3+4)7*749

วิธีการที่แตกต่างกัน

ก่อนอื่นสมมติว่าเรามีฟังก์ชั่นที่มีผลข้างเคียง ฟังก์ชั่นนี้จะพิมพ์สิ่งที่ต้องการแล้วส่งกลับค่า Int

def something() = {
  println("calling something")
  1 // return value
}

ตอนนี้เรากำลังจะกำหนดฟังก์ชั่นสองตัวที่รับอาร์กิวเมนต์ของ Int ที่เหมือนกันทุกประการยกเว้นฟังก์ชั่นนั้นจะรับอาร์กิวเมนต์ในรูปแบบการโทรตามค่า (x: Int) และอีกฟังก์ชันหนึ่งในรูปแบบการโทรตามชื่อ (x: => Int)

def callByValue(x: Int) = {
  println("x1=" + x)
  println("x2=" + x)
}
def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

ตอนนี้เกิดอะไรขึ้นเมื่อเราเรียกพวกมันมาพร้อมกับฟังก์ชั่นลดผลกระทบ

scala> callByValue(something())
calling something
x1=1
x2=1
scala> callByName(something())
calling something
x1=1
calling something
x2=1

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

นี่เป็นเพราะฟังก์ชั่นการโทรตามค่าคำนวณค่าของนิพจน์แบบพาส - อินก่อนเรียกใช้ฟังก์ชันดังนั้นจึงมีการเข้าถึงค่าเดียวกันทุกครั้ง อย่างไรก็ตามฟังก์ชัน call-by-name จะคำนวณค่าของนิพจน์แบบพาส - อินใหม่ทุกครั้งที่เข้าถึง

ตัวอย่างที่ดีกว่าในการใช้งาน CALL-BY-NAME

จาก: https://stackoverflow.com/a/19036068/1773841

ตัวอย่างประสิทธิภาพการทำงานที่เรียบง่าย: การบันทึก

ลองจินตนาการถึงอินเทอร์เฟซแบบนี้:

trait Logger {
  def info(msg: => String)
  def warn(msg: => String)
  def error(msg: => String)
}

แล้วใช้แบบนี้:

logger.info("Time spent on X: " + computeTimeSpent)

หากวิธีการข้อมูลไม่ได้ทำอะไร (เพราะพูดว่าระดับการบันทึกถูกกำหนดค่าให้สูงกว่านั้น) จากนั้น computeTimeSpent จะไม่ถูกเรียกใช้ซึ่งจะช่วยประหยัดเวลา สิ่งนี้เกิดขึ้นมากมายกับตัวบันทึกซึ่งมักจะเห็นการจัดการสตริงซึ่งอาจมีราคาแพงเมื่อเทียบกับงานที่ถูกบันทึก

ตัวอย่างความถูกต้อง: ตัวดำเนินการเชิงตรรกะ

คุณอาจเห็นโค้ดเช่นนี้:

if (ref != null && ref.isSomething)

ลองจินตนาการว่าคุณจะประกาศ && วิธีเช่นนี้:

trait Boolean {
  def &&(other: Boolean): Boolean
}

จากนั้นเมื่อใดก็ตามที่การอ้างอิงเป็นโมฆะคุณจะได้รับข้อผิดพลาดเนื่องจาก isSomething จะถูกเรียกในการอ้างอิงก่อนที่จะถูกส่งผ่านไปยัง && ด้วยเหตุผลนี้การประกาศจริงคือ:

trait Boolean {
  def &&(other: => Boolean): Boolean =
    if (this) this else other
}

1

การดูตัวอย่างจะช่วยให้คุณเข้าใจความแตกต่างได้ดีขึ้น

มาเรียนฟังก์ชั่นง่ายๆที่คืนค่าเวลาปัจจุบัน:

def getTime = System.currentTimeMillis

ตอนนี้เราจะกำหนดฟังก์ชั่นตามชื่อที่พิมพ์ล่าช้าสองครั้งภายในหนึ่งวินาที:

def getTimeByName(f: => Long) = { println(f); Thread.sleep(1000); println(f)}

และหนึ่งค่า :

def getTimeByValue(f: Long) = { println(f); Thread.sleep(1000); println(f)}

ตอนนี้ให้โทรหากัน:

getTimeByName(getTime)
// prints:
// 1514451008323
// 1514451009325

getTimeByValue(getTime)
// prints:
// 1514451024846
// 1514451024846

ผลลัพธ์ควรอธิบายความแตกต่าง ข้อมูลสามารถใช้ได้ที่นี่


0

CallByNameถูกเรียกใช้เมื่อถูกใช้และcallByValueถูกเรียกใช้เมื่อใดก็ตามที่พบคำสั่ง

ตัวอย่างเช่น:-

ฉันมีวงวนไม่สิ้นสุดคือถ้าคุณใช้งานฟังก์ชั่นนี้เราจะไม่ได้รับscalaพรอมต์

scala> def loop(x:Int) :Int = loop(x-1)
loop: (x: Int)Int

callByNameฟังก์ชั่นใช้เวลาดังกล่าวข้างต้นloopวิธีการเป็นอาร์กิวเมนต์และมันก็ไม่เคยใช้ภายในร่างกายของตน

scala> def callByName(x:Int,y: => Int)=x
callByName: (x: Int, y: => Int)Int

ในการดำเนินการของcallByNameวิธีการที่เราไม่พบปัญหาใด ๆ (เราได้รับscalaพรอมต์กลับ) เนื่องจากเราไม่มีที่ใช้ฟังก์ชั่นวงภายในcallByNameฟังก์ชั่น

scala> callByName(1,loop(10))
res1: Int = 1
scala> 

callByValueฟังก์ชั่นใช้เวลาดังกล่าวข้างต้นloopวิธีการเป็นพารามิเตอร์เป็นผลภายในฟังก์ชั่นหรือการแสดงออกที่ถูกประเมินก่อนที่จะดำเนินฟังก์ชั่นด้านนอกโดยมีloopฟังก์ชั่นการดำเนินการซ้ำและเราไม่เคยได้รับscalaกลับพร้อมท์

scala> def callByValue(x:Int,y:Int) = x
callByValue: (x: Int, y: Int)Int

scala> callByValue(1,loop(1))

0

ดูนี่:

    object NameVsVal extends App {

  def mul(x: Int, y: => Int) : Int = {
    println("mul")
    x * y
  }
  def add(x: Int, y: Int): Int = {
    println("add")
    x + y
  }
  println(mul(3, add(2, 1)))
}

y: => Int คือการโทรตามชื่อ สิ่งที่ถูกส่งเป็นชื่อเรียกจะเพิ่ม (2, 1) สิ่งนี้จะถูกประเมินอย่างขี้เกียจ ดังนั้นเอาต์พุตบนคอนโซลจะเป็น "mul" ตามด้วย "เพิ่ม" แม้ว่าการเพิ่มดูเหมือนจะถูกเรียกก่อน การโทรตามชื่อทำหน้าที่เหมือนกับการส่งต่อตัวชี้ฟังก์ชัน
ตอนนี้เปลี่ยนจาก y: => Int เป็น y: Int คอนโซลจะแสดง "เพิ่ม" ตามด้วย "mul"! วิธีการประเมินตามปกติ


-2

ฉันไม่คิดว่าคำตอบทั้งหมดที่นี่ให้เหตุผลที่ถูกต้อง:

การเรียกตามค่าอาร์กิวเมนต์จะถูกคำนวณเพียงครั้งเดียว:

def f(x : Int, y :Int) = x

// following the substitution model

f(12 + 3, 4 * 11)
f(15, 4194304)
15

คุณสามารถเห็นข้างต้นว่าข้อโต้แย้งทั้งหมดจะได้รับการประเมินว่าไม่จำเป็นโดยปกติแล้วcall-by-valueจะเร็ว แต่ไม่เหมือนในกรณีนี้

หากกลยุทธ์การประเมินผลเป็นเช่นcall-by-nameนั้นการสลายตัวจะเป็นดังนี้:

f(12 + 3, 4 * 11)
12 + 3
15

ดังที่คุณเห็นข้างต้นเราไม่จำเป็นต้องประเมิน4 * 11และบันทึกการคำนวณเล็กน้อยซึ่งอาจเป็นประโยชน์ในบางครั้ง

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