ผลข้างเคียงทำลายความโปร่งใสในการอ้างอิง


11

ฟังก์ชั่นการเขียนโปรแกรมใน Scalaอธิบายผลกระทบของผลกระทบต่อการทำลายความโปร่งใสในการอ้างอิง:

ผลข้างเคียงซึ่งแสดงถึงการละเมิดความโปร่งใสในการอ้างอิงบางส่วน

ฉันอ่านส่วนหนึ่งของSICPซึ่งกล่าวถึงการใช้ "รูปแบบการแทนที่" เพื่อประเมินโปรแกรม

เมื่อฉันเข้าใจแบบจำลองการแทนที่ด้วย referential transparent (RT) คร่าวๆคุณสามารถยกเลิกการเขียนฟังก์ชันลงในส่วนที่ง่ายที่สุดได้ หากการแสดงออกเป็น RT แล้วคุณสามารถยกเลิกการเขียนการแสดงออกและมักจะได้รับผลเดียวกัน

อย่างไรก็ตามตามที่ระบุไว้ข้างต้นการใช้ผลข้างเคียงสามารถ / จะทำลายรูปแบบการทดแทน

ตัวอย่าง:

val x = foo(50) + bar(10)

ถ้าfooและbar ไม่ได้มีผลข้างเคียงแล้วดำเนินการอย่างใดอย่างหนึ่งฟังก์ชั่นจะเสมอxกลับผลเดียวกัน แต่หากพวกเขามีผลข้างเคียงพวกเขาจะเปลี่ยนตัวแปรที่ขัดขวาง / โยนประแจเข้ากับโมเดลการแทนที่

ฉันรู้สึกสบายใจกับคำอธิบายนี้ แต่ฉันก็ไม่รู้สึกแย่

โปรดแก้ไขฉันและกรอกข้อมูลในหลุมใด ๆ ที่เกี่ยวกับผลข้างเคียงที่ทำให้ RT แตกหักพูดคุยถึงผลกระทบต่อรูปแบบการทดแทนเช่นกัน

คำตอบ:


20

เริ่มต้นด้วยคำนิยามเพื่อความโปร่งใสในการอ้างอิง :

นิพจน์จะกล่าวว่ามีความโปร่งใสในการอ้างอิงถ้ามันสามารถถูกแทนที่ด้วยค่าของมันโดยไม่เปลี่ยนพฤติกรรมของโปรแกรม

หมายความว่าอะไร (ตัวอย่าง) คุณสามารถแทนที่ 2 + 5 ด้วย 7 ในส่วนใด ๆ ของโปรแกรมและโปรแกรมควรจะยังคงทำงาน กระบวนการนี้เรียกว่าการทดแทน ชดเชยที่ถูกต้องถ้าหากว่า 2 + 5 สามารถถูกแทนที่ด้วย 7 โดยไม่ต้องมีผลกระทบต่อส่วนอื่น ๆ ของโปรแกรม

สมมติว่าฉันมีชั้นเรียนที่เรียกว่าBazมีฟังก์ชั่นFooและBarในนั้น เพื่อความเรียบง่ายเราจะพูดอย่างนั้นFooและBarคืนค่าที่ส่งผ่านดังนั้นFoo(2) + Bar(5) == 7ตามที่คุณคาดหวัง Referential Transparency รับประกันว่าคุณสามารถแทนที่การแสดงออกFoo(2) + Bar(5)ด้วยการแสดงออก7ที่ใดก็ได้ในโปรแกรมของคุณและโปรแกรมจะยังคงทำงานเหมือนกัน

แต่ถ้าFooกลับค่าที่ส่งไปใน แต่Barกลับค่าที่ส่งในบวกค่าสุดท้ายที่ให้ไว้เพื่อFoo? มันง่ายพอที่จะทำถ้าคุณเก็บค่าFooไว้ในตัวแปรท้องถิ่นภายในBazคลาส ถ้าค่าเริ่มต้นของตัวแปรโลคัลเป็น 0 นิพจน์Foo(2) + Bar(5)จะคืนค่าที่คาดไว้ของ7ครั้งแรกที่คุณเรียกใช้ แต่จะคืน9ค่าในครั้งที่สองที่คุณเรียกใช้

นี่เป็นการละเมิดความโปร่งใสของการอ้างอิงสองวิธี ก่อนอื่นแถบจะไม่สามารถนับเพื่อส่งคืนนิพจน์เดิมทุกครั้งที่เรียกได้ ประการที่สองเกิดผลข้างเคียงกล่าวคือการเรียก Foo มีผลต่อค่าตอบแทนของ Bar เนื่องจากคุณไม่สามารถรับประกันได้ว่าFoo(2) + Bar(5)จะเท่ากับ 7 คุณจึงไม่สามารถแทนที่ได้อีกต่อไป

นี่คือความโปร่งใสของ Referential Transceivers ในทางปฏิบัติ ฟังก์ชันโปร่งใสที่อ้างอิงได้ยอมรับค่าบางค่าและส่งคืนค่าที่สอดคล้องกันโดยไม่ส่งผลกระทบต่อโค้ดอื่น ๆ ในโปรแกรมและส่งคืนเอาต์พุตเดียวกันที่ให้อินพุตเดียวกันเสมอ


5
ดังนั้นการRTปิดการใช้งานคุณจากการใช้substitution model.ปัญหาใหญ่ที่ไม่สามารถใช้งานได้substitution modelคือพลังของการใช้มันเพื่อเหตุผลเกี่ยวกับโปรแกรมหรือไม่
เควินเมเรดิ ธ

ถูกต้องแล้ว
Robert Harvey

1
+1 คำตอบที่ชัดเจนและเข้าใจได้อย่างน่าอัศจรรย์ ขอบคุณ.
Racheet

2
นอกจากนี้หากฟังก์ชั่นเหล่านั้นมีความโปร่งใสหรือ "บริสุทธิ์" ลำดับที่พวกเขาทำงานจริง ๆ นั้นไม่สำคัญเราไม่สนใจว่า foo () หรือ bar () ทำงานก่อนและในบางกรณีพวกเขาอาจไม่เคยประเมินว่าพวกเขาไม่ต้องการ
Zachary K

1
อีกข้อได้เปรียบของ RT คือสามารถใช้แคชที่มีการอ้างอิงโปร่งใสที่มีราคาแพง (เนื่องจากการประเมินพวกเขาหนึ่งหรือสองครั้งควรให้ผลลัพธ์ที่เหมือนกัน)
dcastro

3

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

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

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

  • บางคนมีแม่เหล็กที่แข็งแกร่งและจะผลักกล่องแม่เหล็กอื่น ๆ ออกมาจากผนังหากจัดแนวที่ไม่เหมาะสม
  • บางอันร้อนจัดหรือเย็นจัดและจะตอบสนองไม่ดีหากวางไว้ในพื้นที่ใกล้เคียง

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

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

ในภาษาที่จำเป็นคุณจะไม่มีทางรู้ว่าสิ่งที่น่าประหลาดใจอาจซ่อนอยู่ข้างใน


"ในภาษาที่ใช้งานได้จริงสิ่งที่คุณต้องเห็นคือลายเซ็นของฟังก์ชั่นเพื่อให้รู้ว่ามันทำอะไร" - ไม่เป็นความจริง ใช่ภายใต้สมมติฐานของ polymorphism ตัวแปรที่เราสามารถสรุปได้ว่าเป็นหน้าที่ของประเภท(a, b) -> aเท่านั้นที่สามารถจะเป็นfstฟังก์ชั่นและฟังก์ชั่นชนิดa -> aเท่านั้นที่สามารถจะเป็นidentityฟังก์ชั่น แต่คุณไม่สามารถจำเป็นต้องพูดอะไรเกี่ยวกับการทำงานของประเภท(a, a) -> aเช่น
Jörg W Mittag

2

เมื่อฉันเข้าใจแบบจำลองการแทนที่ (ด้วย referential transparent (RT)) คร่าวๆคุณสามารถยกเลิกการจัดทำฟังก์ชันเป็นส่วนที่ง่ายที่สุดได้ หากการแสดงออกเป็น RT คุณสามารถยกเลิกการแสดงออกและได้รับผลลัพธ์เดียวกัน

ใช่ปรีชาค่อนข้างถูกต้อง ต่อไปนี้เป็นตัวชี้เพื่อให้แม่นยำยิ่งขึ้น:

เช่นเดียวกับที่คุณพูดนิพจน์ RT ควรมีsingle"ผลลัพธ์" นั่นคือเมื่อได้รับfactorial(5)นิพจน์ในโปรแกรมก็ควรให้ผลลัพธ์ "ผลลัพธ์" ที่เหมือนกันเสมอ ดังนั้นถ้าบางอย่างfactorial(5)อยู่ในโปรแกรมและผลตอบแทนถัวเฉลี่ย 120 ก็ควรถัวเฉลี่ย 120 โดยไม่คำนึงถึง "สั่งขั้นตอน" มันมีการขยาย / คำนวณ - ไม่คำนึงถึงเวลา

ตัวอย่าง: factorialฟังก์ชั่น

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

มีข้อควรพิจารณาบางประการเกี่ยวกับคำอธิบายนี้

ก่อนอื่นโปรดจำไว้ว่ารูปแบบการประเมินที่แตกต่างกัน (ดูที่การสมัครกับลำดับปกติ) อาจให้ "ผลลัพธ์" ที่แตกต่างกันสำหรับนิพจน์ RT เดียวกัน

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

ในโค้ดข้างต้นfirstและsecondมีความโปร่งใสในการอ้างอิงและยังนิพจน์ที่ส่วนท้ายให้ผลลัพธ์ "ผลลัพธ์" ที่แตกต่างกันหากประเมินภายใต้คำสั่งปกติและคำสั่งการสมัคร (ภายใต้หลังนิพจน์จะไม่หยุด)

.... ซึ่งนำไปสู่การใช้ "ผลลัพธ์" ในเครื่องหมายคำพูด เนื่องจากมันไม่จำเป็นต้องมีการแสดงออกที่จะหยุดมันอาจไม่ก่อให้เกิดมูลค่า ดังนั้นการใช้ "ผลลัพธ์" จึงเป็นพร่ามัว เราสามารถพูดได้ว่านิพจน์ RT ให้ผลเหมือนกันเสมอcomputationsภายใต้โมเดลการประเมิน

ประการที่สามมันอาจจะต้องเห็นสองfoo(50)ปรากฏในโปรแกรมในสถานที่ที่แตกต่างกันเป็นนิพจน์ที่แตกต่างกัน - แต่ละคนให้ผลลัพธ์ของตัวเองที่อาจแตกต่างกัน ตัวอย่างเช่นหากภาษาอนุญาตให้ใช้ขอบเขตแบบไดนามิกทั้งสองนิพจน์แม้จะเหมือนกันทุกประการจะแตกต่างกัน ใน Perl:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

แบบไดนามิก misleads ขอบเขตเพราะมันทำให้มันง่ายที่ใครจะคิดว่าxเป็นเพียง แต่สำหรับการป้อนข้อมูลfooเมื่อในความเป็นจริงมันเป็นและx yวิธีหนึ่งที่จะเห็นความแตกต่างคือการแปลงโปรแกรมให้เทียบเท่ากันโดยไม่มีขอบเขตแบบไดนามิกนั่นคือผ่านพารามิเตอร์อย่างชัดเจนดังนั้นแทนที่จะกำหนดfoo(x)เรากำหนดfoo(x, y)และส่งผ่านyอย่างชัดเจนในผู้โทร

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

ทีนี้รหัสต่อไปนี้คืออะไร?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

fooขั้นตอนการแบ่ง RT เพราะมี redefinitions นั่นก็คือเรากำหนดไว้yในจุดหนึ่งและหลังในนิยามใหม่ที่เดียวกัน yในตัวอย่าง perl ด้านบนys มีการผูกที่แตกต่างกันแม้ว่าจะใช้ชื่อตัวอักษรเดียวกัน "y" ที่นี่yของจริงเหมือนกัน นั่นเป็นเหตุผลที่เราพูดว่า (อีกครั้ง) การกำหนดเป็นการดำเนินการเมตา : คุณกำลังเปลี่ยนคำจำกัดความของโปรแกรมของคุณ

input -> outputประมาณคนมักจะแสดงให้เห็นถึงความแตกต่างดังนี้ในการตั้งค่าฟรีผลข้างเคียงคุณมีการทำแผนที่จาก ในการตั้งค่า "จำเป็น" คุณมีinput -> ouputในบริบทของstateที่สามารถเปลี่ยนแปลงได้ตลอดเวลา

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

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


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