ฟังก์ชั่นที่บริสุทธิ์:“ ไม่มีผลข้างเคียง” หมายถึง“ ผลลัพธ์เดียวกันเสมอให้อินพุตเดียวกัน” หรือไม่?


84

เงื่อนไขสองประการที่กำหนดฟังก์ชันpureดังต่อไปนี้:

  1. ไม่มีผลข้างเคียง (เช่นอนุญาตให้เปลี่ยนแปลงขอบเขตเฉพาะที่เท่านั้น)
  2. ส่งคืนเอาต์พุตเดียวกันเสมอโดยให้อินพุตเดียวกัน

หากเงื่อนไขแรกเป็นจริงเสมอมีบางครั้งเงื่อนไขที่สองไม่เป็นจริงหรือไม่?

คือจำเป็นจริงๆกับเงื่อนไขแรกหรือไม่?


3
สถานที่ของคุณระบุไม่ถูกต้อง "อินพุต" กว้างเกินไป ฟังก์ชั่นสามารถคิดได้สองประเภทมีอินพุต ข้อโต้แย้งและ "สิ่งแวดล้อม" / "บริบท" ฟังก์ชันที่ส่งคืนเวลาของระบบอาจคิดว่าบริสุทธิ์ (แม้ว่าจะไม่ใช่ obv) หากคุณไม่แยกความแตกต่างระหว่างอินพุตสองประเภทนี้
Alexander

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

3
ตัวอย่างการตอบโต้เล็กน้อย: ส่งคืนค่าของตัวแปรส่วนกลาง ไม่มีผลข้างเคียง (ทั่วโลกเท่านั้นที่เคยอ่าน!) แต่ผลลัพธ์อาจแตกต่างกันทุกครั้ง (หากคุณไม่ชอบ globals ให้ส่งคืนที่อยู่ของตัวแปรโลคัลซึ่งขึ้นอยู่กับ call stack ในขณะทำงาน)
ปีเตอร์ - คืนสถานะโมนิกา

2
คุณต้องขยายคำจำกัดความของ "ผลข้างเคียง"; คุณบอกว่าวิธีการที่บริสุทธิ์ไม่ก่อให้เกิดผลข้างเคียง แต่คุณต้องสังเกตด้วยว่าวิธีการที่บริสุทธิ์ไม่ใช้ผลข้างเคียงที่ผลิตจากที่อื่น
Eric Lippert

2
@sleske บางทีเป็นที่เข้าใจกันโดยทั่วไป แต่การขาดความแตกต่างนั้นเป็นสาเหตุที่แท้จริงของความสับสนของ OP
Alexander

คำตอบ:


114

นี่คือตัวอย่างบางส่วนที่ไม่เปลี่ยนขอบเขตภายนอก แต่ยังถือว่าไม่บริสุทธิ์:

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); } (ซึ่งยอมรับว่าเปลี่ยน PRNG แต่ไม่ถือว่าสังเกตได้)

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

ฉันมักจะนึกถึงสองเงื่อนไขสำหรับความบริสุทธิ์ที่เสริมกัน:

  • การประเมินผลลัพธ์จะต้องไม่มีผลกระทบต่อสภาวะข้างเคียง
  • ผลการประเมินจะต้องไม่ได้รับผลกระทบจากสถานะข้างเคียง

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


1
ขอบคุณ Bergi ด้วยเหตุผลบางอย่างฉันคิดว่าผลข้างเคียงรวมถึงตัวแปรการอ่านนอกขอบเขตเฉพาะที่ แต่ฉันเดาว่ามันเป็นเพียงผลข้างเคียงถ้ามันเขียนตัวแปรภายนอกดังกล่าว
Magnus

17
หากprompt("you choose")ไม่มีผลข้างเคียงเราควรถอยหลังและชี้แจงความหมายของผลข้างเคียง
Holger

1
@ แม็กนัสใช่นั่นคือความหมายของผล ฉันจะพยายามชี้แจงในคำตอบของฉันเช่นกันฉันไม่ได้คาดหวังความสนใจมากขนาดนี้และต้องการให้คำตอบที่คุ้มค่ากับคะแนนโหวตมากมาย :-)
Bergi

2
สำหรับทุกสิ่งที่คุณรู้ Math.random () ส่งคืนไดโอดความร้อน ไม่ได้ระบุให้ใช้ RNG ที่ไม่ดี
Joshua

1
จากสองเงื่อนไขนี้ฉันเคยได้ยินชื่อเดิมเรียกว่า "เอฟเฟกต์" ในขณะที่เงื่อนไขหลังเรียกว่า "ผลกระทบ" ทั้งสองเป็น "ผลข้างเคียง" และไม่บริสุทธิ์ f (coeffects, input) -> effects, output Coeffects คืออินพุตที่มาจากการเปลี่ยนแปลงของสภาพแวดล้อมที่กว้างขึ้นเอฟเฟกต์คือเอาต์พุตที่เปลี่ยนสภาพแวดล้อมที่กว้างขึ้น ตัวอย่างเช่น Elm และ Clojurescrips จะใช้กรอบใหม่กับโมเดลนี้

30

"ปกติ" วิธีการใช้ถ้อยคำสิ่งที่ฟังก์ชั่นที่บริสุทธิ์คือเป็นในแง่ของความโปร่งใสอ้างอิง ฟังก์ชั่นคือบริสุทธิ์ถ้ามันเป็นความโปร่งใส referentially

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

ตัวอย่างเช่นหาก C printfมีความโปร่งใสอ้างอิงทั้งสองโปรแกรมควรมีความหมายเหมือนกัน:

printf("Hello");

และ

5;

และโปรแกรมทั้งหมดต่อไปนี้ควรมีความหมายเหมือนกัน:

5 + 5;

printf("Hello") + 5;

printf("Hello") + printf("Hello");

เนื่องจากprintfส่งคืนจำนวนอักขระที่เขียนในกรณีนี้ 5.

จะชัดเจนยิ่งขึ้นด้วยvoidฟังก์ชันต่างๆ ถ้าฉันมีฟังก์ชั่นvoid fooแล้ว

foo(bar, baz, quux);

ควรจะเหมือนกับ

;

กล่าวคือเนื่องจากfooไม่ส่งคืนอะไรเลยฉันควรจะสามารถแทนที่ได้โดยไม่ต้องเปลี่ยนความหมายของโปรแกรม

เป็นที่ชัดเจนแล้วว่าค่าprintfมิได้fooมีความโปร่งใส referentially และทำให้ค่าของพวกเขาบริสุทธิ์ ในความเป็นจริงvoidฟังก์ชันไม่สามารถอ้างอิงได้อย่างโปร่งใสเว้นแต่จะเป็นแบบไม่ใช้งาน

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

func fib(n):
    return memo[n] if memo.has_key?(n)
    return 1 if n <= 1
    return memo[n] = fib(n-1) + fib(n-2)

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

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


2
ความไม่บริสุทธิ์นั้นส่งผลต่อโปรแกรมทั้งหมดเมื่อคุณมีภาวะพร้อมกัน
R .. GitHub STOP HELPING ICE

@R .. คุณนึกถึงวิธีที่การทำงานพร้อมกันสามารถทำให้ฟังก์ชัน Fibonacci ที่อธิบายไว้ภายนอกไม่บริสุทธิ์ได้หรือไม่? ฉันทำไม่ได้ การเขียนถึงmemo[n]เป็นเรื่องปกติและการไม่สามารถอ่านได้ทำให้เสียรอบการทำงานของ CPU เท่านั้น
Brilliand

ฉันเห็นด้วยกับทั้งสองคน ความไม่บริสุทธิ์อาจนำไปสู่ปัญหาการเกิดพร้อมกัน แต่ไม่ได้เกิดขึ้นในกรณีนี้
Jörg W Mittag

@R .. ไม่ยากที่จะจินตนาการถึงเวอร์ชันที่รับรู้พร้อมกัน
user253751

1
@Brilliand ตัวอย่างเช่นmemo[n] = ...อาจสร้างรายการพจนานุกรมก่อนจากนั้นจึงเก็บค่าไว้ในนั้น ซึ่งจะออกจากหน้าต่างในระหว่างที่เธรดอื่นสามารถมองเห็นรายการที่ไม่ได้กำหนดค่าเริ่มต้น
user253751

12

สำหรับฉันแล้วดูเหมือนว่าเงื่อนไขที่สองที่คุณอธิบายนั้นเป็นข้อ จำกัด ที่อ่อนแอกว่าเงื่อนไขแรก

ฉันขอยกตัวอย่างสมมติว่าคุณมีฟังก์ชันที่จะเพิ่มฟังก์ชันที่บันทึกลงในคอนโซลด้วย:

function addOneAndLog(x) {
  console.log(x);
  return x + 1;
}

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

ฟังก์ชั่นที่บริสุทธิ์คือพูดอย่างเคร่งครัด, ฟังก์ชั่นที่ตอบสนองคุณสมบัติของความโปร่งใสอ้างอิง นั่นคือคุณสมบัติที่เราสามารถแทนที่แอพพลิเคชั่นฟังก์ชันด้วยค่าที่สร้างขึ้นโดยไม่เปลี่ยนพฤติกรรมของโปรแกรม

สมมติว่าเรามีฟังก์ชันที่เพิ่ม:

function addOne(x) {
  return x + 1;
}

เราสามารถแทนที่addOne(5)ด้วย6ที่ใดก็ได้ในโปรแกรมของเราและจะไม่มีอะไรเปลี่ยนแปลง

ในทางตรงกันข้ามเราไม่สามารถแทนที่addOneAndLog(x)ด้วยค่า6ที่ใดก็ได้ในโปรแกรมของเราโดยไม่เปลี่ยนพฤติกรรมเนื่องจากนิพจน์แรกส่งผลให้มีบางสิ่งถูกเขียนไปยังคอนโซลในขณะที่นิพจน์ที่สองไม่ได้

เราพิจารณาใด ๆ ของพฤติกรรมนี้พิเศษที่addOneAndLog(x)ดำเนินการนอกเหนือจากการกลับมาส่งออกเป็นผลข้างเคียง


"สำหรับฉันแล้วดูเหมือนว่าเงื่อนไขที่สองที่คุณอธิบายนั้นเป็นข้อ จำกัด ที่อ่อนแอกว่าเงื่อนไขแรก ไม่ทั้งสองเงื่อนไขเป็นอิสระทางตรรกะ
sleske

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

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

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

2
ดูเหมือนว่าคุณกำลังใช้คำจำกัดความของ "ผลข้างเคียง" ที่แตกต่างจากที่ใช้กันทั่วไป ผลข้างเคียงที่มีการกำหนดกันว่า "ผลที่สังเกตได้นอกเหนือจากการกลับค่า" หรือ "การเปลี่ยนแปลงที่สังเกตได้ในรัฐ" - เห็นเช่นวิกิพีเดีย , โพสต์ใน softwareengineering.SE คุณพูดถูกทั้งหมดที่Date.now()ไม่โปร่งใส / อ้างอิง แต่ไม่ใช่เพราะมีผลข้างเคียง แต่เนื่องจากผลลัพธ์ขึ้นอยู่กับข้อมูลที่ป้อนเข้ามากกว่า
sleske

7

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

ทั้งหมดที่ฉันคิดได้อย่างไรก็ตาม


3
ตามที่ฉันพูด "การสุ่มจากนอกระบบ" เหล่านี้เป็นผลข้างเคียงรูปแบบหนึ่ง หน้าที่ที่มีพฤติกรรมเหล่านี้ไม่ใช่ "เพียว"
Joseph

2

ปัญหาเกี่ยวกับคำจำกัดความของ FP คือมันเป็นสิ่งประดิษฐ์มาก การประเมิน / การคำนวณแต่ละครั้งมีผลข้างเคียงต่อผู้ประเมิน มันเป็นความจริงในทางทฤษฎี การปฏิเสธสิ่งนี้แสดงให้เห็นเพียงว่าผู้ขอโทษ FP เพิกเฉยต่อปรัชญาและตรรกะ: "การประเมินผล" หมายถึงการเปลี่ยนแปลงสถานะของสภาพแวดล้อมอัจฉริยะบางอย่าง (เครื่องจักรสมอง ฯลฯ ) นี่คือลักษณะของกระบวนการประเมิน ไม่มีการเปลี่ยนแปลง - ไม่มี "นิ่ว" ผลกระทบสามารถมองเห็นได้ชัดเจน: ความร้อนของ CPU หรือความล้มเหลวการปิดเมนบอร์ดในกรณีที่มีความร้อนสูงเกินไปและอื่น ๆ

เมื่อคุณพูดถึงความโปร่งใสในการอ้างอิงคุณควรเข้าใจว่าข้อมูลเกี่ยวกับความโปร่งใสดังกล่าวมีให้สำหรับมนุษย์ในฐานะผู้สร้างทั้งระบบและผู้ถือข้อมูลเชิงความหมายและอาจไม่มีให้สำหรับคอมไพเลอร์ ตัวอย่างเช่นฟังก์ชันสามารถอ่านทรัพยากรภายนอกบางส่วนและจะมี IO monad ในลายเซ็น แต่จะส่งคืนค่าเดิมตลอดเวลา (เช่นผลลัพธ์ของcurrent_year > 0) คอมไพลเลอร์ไม่ทราบว่าฟังก์ชันจะส่งคืนผลลัพธ์เดียวกันเสมอดังนั้นฟังก์ชันจึงไม่บริสุทธิ์ แต่มีคุณสมบัติโปร่งใสอ้างอิงและสามารถแทนที่ด้วยTrueค่าคงที่ได้

ดังนั้นเพื่อหลีกเลี่ยงความไม่ถูกต้องดังกล่าวเราควรแยกแยะฟังก์ชันทางคณิตศาสตร์และ "ฟังก์ชัน" ในภาษาโปรแกรม ฟังก์ชั่นใน Haskell มักไม่บริสุทธิ์และคำจำกัดความของความบริสุทธิ์ที่เกี่ยวข้องนั้นมีเงื่อนไขอยู่เสมอ: พวกเขากำลังทำงานบนฮาร์ดแวร์จริงที่มีผลข้างเคียงและคุณสมบัติทางกายภาพจริงซึ่งไม่ถูกต้องสำหรับฟังก์ชันทางคณิตศาสตร์ ซึ่งหมายความว่าตัวอย่างที่มีฟังก์ชัน "printf" ไม่ถูกต้องทั้งหมด

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


2

หากเงื่อนไขแรกเป็นจริงเสมอมีบางครั้งเงื่อนไขที่สองไม่เป็นจริงหรือไม่?

ใช่

พิจารณาข้อมูลโค้ดอย่างง่ายด้านล่าง

public int Sum(int a, int b) {
    Random rnd = new Random();
    return rnd.Next(1, 10);
}

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

ผลกระทบโดยรวมของทั้งสองจุด # 1 และ # 2 ที่คุณกล่าวถึงเมื่อรวมกันหมายถึง: ณ จุดเวลาใด ๆ ถ้าฟังก์ชั่นSumแบบเดียวกับฉัน / p จะถูกแทนที่ด้วยผลในโปรแกรมความหมายโดยรวมของโปรแกรมไม่เปลี่ยนแปลง นี้คืออะไร แต่อ้างอิงความโปร่งใส


แต่ในกรณีนี้เงื่อนไขแรกจะไม่ได้รับการตรวจสอบ: การเขียนลงคอนโซลถือเป็นผลข้างเคียงเนื่องจากจะเปลี่ยนสถานะของเครื่องเอง
ขาขวา

@Rightleg ขอบคุณสำหรับการชี้ให้เห็น ฉันเข้าใจ OP ผิดไปทางอื่นโดยสิ้นเชิง คำตอบที่ถูกต้อง
rahulaga_dev

2
มันไม่เปลี่ยนสถานะของเครื่องกำเนิดไฟฟ้าแบบสุ่ม?
Eric Duminil

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

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