คุณจะทำอะไรที่มีประโยชน์โดยไม่เปลี่ยนสถานะได้อย่างไร


265

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

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

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

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


2

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

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

7
สั้น ๆ ใน FP ฟังก์ชั่นไม่เคยแก้ไขสถานะ ในที่สุดพวกเขาก็จะคืนสิ่งที่เข้ามาแทนที่สถานะปัจจุบัน แต่รัฐจะไม่แก้ไข (กลายพันธุ์) ในสถานที่
jinglesthula

มีวิธีที่จะได้รับสถานะโดยไม่ต้องกลายพันธุ์ (ใช้สแต็คจากสิ่งที่ฉันเข้าใจ) แต่คำถามนี้อยู่ในความรู้สึกข้างจุด (แม้ว่ามันจะเป็นเรื่องที่ดี) ยากที่จะพูดคุยเกี่ยวกับชัดถ้อยชัดคำ แต่นี่เป็นโพสต์ซึ่งหวังว่าตอบคำถามของคุณmedium.com/@jbmilgrom/... TLDR คือความหมายของแม้แต่โปรแกรมการทำงานแบบ stateful นั้นไม่เปลี่ยนรูปอย่างไรก็ตามการสื่อสาร b / w ของฟังก์ชั่นโปรแกรมจะได้รับการจัดการ
jbmilgrom

คำตอบ:


166

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

หากคุณสนใจนี่คือบทความเกี่ยวกับการเขียนโปรแกรมเกมกับ Erlang

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

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

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

มันไม่สวยมาก แต่เราได้ผลแบบเดียวกันโดยไม่มีการกลายพันธุ์ แน่นอนทุกที่ที่เป็นไปได้เราชอบหลีกเลี่ยงการวนซ้ำทั้งหมดและแยกมันออกไป

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

วิธี Seq.iter จะแจกแจงผ่านการรวบรวมและเรียกใช้ฟังก์ชันที่ไม่ระบุชื่อสำหรับแต่ละรายการ มีประโยชน์มาก :)

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

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

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

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

ทุกแอปพลิเคชันผู้ใช้ที่ฉันสามารถนึกถึงเกี่ยวข้องเป็นแนวคิดหลัก

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

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

รหัสด้านบนสร้างรายการที่ไม่เปลี่ยนรูปแบบสองรายการผนวกเข้าด้วยกันเพื่อสร้างรายการใหม่และผนวกผลลัพธ์ ไม่มีสถานะที่ไม่แน่นอนถูกใช้ในแอปพลิเคชัน มันดูเทอะทะเล็กน้อย แต่เป็นเพราะ C # เป็นภาษา verbose นี่คือโปรแกรมที่เทียบเท่าใน F #:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

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

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

การเขียนโปรแกรมไร้สัญชาติเป็นเพียงการแสดงออกและมีประสิทธิภาพเช่นเดียวกับการเขียนโปรแกรม stateful มันก็ต้องมีการฝึกฝนเล็กน้อยเพื่อฝึกฝนตัวเองให้เริ่มคิดไร้สัญชาติ แน่นอน "การเขียนโปรแกรมไร้สัญชาติเมื่อเป็นไปได้การเขียนโปรแกรมไร้สัญชาติหากจำเป็น" ดูเหมือนจะเป็นคำขวัญของภาษาที่ใช้งานไม่ได้ส่วนใหญ่ ไม่มีอันตรายใด ๆ หากย้อนกลับไปสู่ความไม่แน่นอนเมื่อแนวทางการทำงานนั้นไม่สะอาดหรือมีประสิทธิภาพ


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

9
เห็นได้ชัดว่าคุณต้องสร้าง "สิ่งอื่น" ใหม่ด้วยวัตถุ Pacman ใหม่;) แน่นอนว่าถ้าเราใช้เส้นทางนั้นมากเกินไปเราจะสร้างกราฟวัตถุขึ้นมาใหม่เพื่อโลกทั้งโลกของเราทุกครั้งที่มีการเปลี่ยนแปลง วิธีการที่ดีกว่านี้ได้อธิบายไว้ที่นี่ ( prog21.dadgum.com/26.html ): แทนที่จะมีการอัปเดตวัตถุเองและการอ้างอิงทั้งหมดมันง่ายกว่ามากที่จะให้พวกเขาส่งข้อความเกี่ยวกับสถานะของพวกเขาไปยังลูปเหตุการณ์ที่จัดการทั้งหมด ปรับปรุง สิ่งนี้ทำให้ง่ายต่อการตัดสินใจว่าวัตถุใดในกราฟที่ต้องมีการอัปเดตและวัตถุใดไม่ต้องการ
Juliet

6
@ จูเลียตฉันมีข้อสงสัย - ในความคิดทั้งหมดของฉันการเรียกซ้ำต้องจบในบางจุดไม่เช่นนั้นคุณจะสร้างกองล้นได้ในที่สุด ในตัวอย่าง pacman แบบเรียกซ้ำสแต็กถูกเก็บไว้ที่เบย์อย่างไร - อ็อบเจ็กต์ถูกเปิดโดยปริยายที่จุดเริ่มต้นของฟังก์ชันหรือไม่?
BlueStrat

9
@ BlueStrat - คำถามที่ดี ... ถ้ามันเป็น "tail call" ... นั่นคือการเรียกซ้ำเป็นสิ่งสุดท้ายในฟังก์ชั่น ... จากนั้นระบบไม่จำเป็นต้องสร้างเฟรมสแต็กใหม่ ... มันสามารถ เพิ่งใช้ซ้ำก่อนหน้านี้ นี่คือการเพิ่มประสิทธิภาพทั่วไปสำหรับภาษาโปรแกรมที่ใช้งานได้ en.wikipedia.org/wiki/Tail_call
reteptilian

4
@MichaelOsofsky เมื่อมีปฏิสัมพันธ์กับฐานข้อมูลและ API มักจะมี 'โลกภายนอก' ที่มีสถานะในการสื่อสารกับ ในกรณีนี้คุณไม่สามารถทำงานได้ 100% เป็นสิ่งสำคัญที่จะต้องแยกรหัส 'ไม่ทำงาน' นี้ออกและแยกออกไปดังนั้นจึงมีเพียงรายการเดียวและทางออกเดียวสู่โลกภายนอก วิธีนี้คุณสามารถทำให้โค้ดที่เหลือของคุณทำงานได้
Chielt

76

คำตอบสั้น ๆ : คุณทำไม่ได้

ดังนั้นความยุ่งยากเกี่ยวกับการเปลี่ยนแปลงไม่ได้แล้วคืออะไร?

หากคุณมีความรอบรู้ในภาษาที่จำเป็นแล้วคุณจะรู้ว่า "กลมไม่ดี" ทำไม? เพราะพวกเขาแนะนำ (หรือมีศักยภาพที่จะแนะนำ) การอ้างอิงบางอย่างที่ไม่อาจแก้ได้ในโค้ดของคุณ และการพึ่งพาไม่ดี คุณต้องการรหัสของคุณจะเป็นแบบแยกส่วน ส่วนของโปรแกรมไม่ส่งผลกระทบต่อส่วนอื่น ๆ น้อยที่สุด และ FP จะนำคุณไปจอกศักดิ์สิทธิ์ของต้นแบบ: ไม่มีผลข้างเคียงที่ทั้งหมด คุณมี f (x) = y ใส่ x เข้า, ออก y ไม่มีการเปลี่ยนแปลง x หรือสิ่งอื่นใด FP ทำให้คุณหยุดคิดถึงสถานะและเริ่มคิดในแง่ของค่านิยม ฟังก์ชั่นทั้งหมดของคุณเพียงแค่รับค่าและสร้างค่าใหม่

มีข้อดีหลายประการ

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

ประการที่สองสิ่งนี้ทำให้โปรแกรมสามารถขนานได้เล็กน้อย (การขนานที่มีประสิทธิภาพเป็นอีกเรื่องหนึ่ง)

ประการที่สามมีข้อได้เปรียบด้านประสิทธิภาพที่เป็นไปได้ สมมติว่าคุณมีฟังก์ชั่น:

double x = 2 * x

ตอนนี้คุณใส่ค่า 3 เข้าไปและคุณได้ค่า 6 ออกมา ทุกเวลา. แต่คุณสามารถทำได้ในสิ่งที่จำเป็นเช่นกันใช่ไหม? อ๋อ แต่ปัญหาคือคุณจำเป็นต้องทำมากกว่านี้ ที่ฉันสามารถทำได้:

int y = 2;
int double(x){ return x * y; }

แต่ฉันก็สามารถทำได้

int y = 2;
int double(x){ return x * (y++); }

คอมไพเลอร์ที่จำเป็นไม่รู้ว่าฉันจะมีผลข้างเคียงหรือไม่ซึ่งทำให้ยากต่อการปรับให้เหมาะสมยิ่งขึ้น (เช่น double 2 ไม่จำเป็นต้องเป็น 4 ทุกครั้ง) ฟังก์ชั่นหนึ่งรู้ว่าฉันจะไม่ - ดังนั้นจึงสามารถปรับให้เหมาะสมทุกครั้งที่เห็น "สองครั้ง"

ตอนนี้แม้ว่าการสร้างค่าใหม่ทุกครั้งดูเหมือนจะสิ้นเปลืองอย่างไม่น่าเชื่อสำหรับค่าชนิดที่ซับซ้อนในแง่ของหน่วยความจำคอมพิวเตอร์ แต่ก็ไม่จำเป็นต้องเป็นเช่นนั้น เพราะถ้าคุณมี f (x) = y และค่า x และ y เป็น "ส่วนใหญ่เหมือนกัน" (เช่นต้นไม้ที่แตกต่างกันเพียงไม่กี่ใบ) จากนั้น x และ y สามารถแบ่งปันส่วนของหน่วยความจำได้ - เพราะทั้งคู่จะไม่กลายพันธุ์ .

ดังนั้นถ้าสิ่งที่ไม่เปลี่ยนแปลงนี้ยอดเยี่ยมมากทำไมฉันตอบว่าคุณไม่สามารถทำอะไรที่มีประโยชน์หากไม่มีสถานะที่ไม่แน่นอน โปรแกรมของคุณทั้งหมดจะเป็นฟังก์ชัน f (x) = y และเช่นเดียวกันสำหรับทุกส่วนของโปรแกรมของคุณ: เพียงแค่ฟังก์ชั่นและฟังก์ชั่นในแง่ที่ "บริสุทธิ์" ในตอนนั้น อย่างที่ฉันพูดนี่หมายถึง f (x) = y ทุกครั้ง ดังนั้นเช่น readFile ("myFile.txt") จะต้องคืนค่าสตริงเดิมทุกครั้ง ไม่มีประโยชน์มากเกินไป

ดังนั้น FP ทุกตัวมีวิธีการกลายพันธุ์บางอย่าง ภาษาที่ใช้งานได้ "บริสุทธิ์" (เช่น Haskell) ทำสิ่งนี้โดยใช้แนวคิดที่น่ากลัวเช่นพระในขณะที่ "ไม่บริสุทธิ์" (เช่น ML) อนุญาตให้ทำสิ่งนี้โดยตรง

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


2
<< readFile ("myFile.txt") จะต้องส่งคืนค่าสตริงเดียวกันทุกครั้ง ไม่เป็นประโยชน์ >> ฉันเดาว่ามันจะมีประโยชน์ตราบใดที่คุณซ่อนระบบไฟล์ทั่วโลก หากคุณพิจารณาว่าเป็นพารามิเตอร์ตัวที่สองและให้กระบวนการอื่นคืนค่าการอ้างอิงใหม่ไปยังระบบไฟล์ทุกครั้งที่พวกเขาแก้ไขด้วย filesystem2 = write (filesystem1, fd, pos, "string") และให้กระบวนการทั้งหมดแลกเปลี่ยนการอ้างอิงไปยังระบบไฟล์ เราจะได้ภาพที่ชัดเจนของระบบปฏิบัติการ
ปลาไหล gEEEE

@eelghEEz นี่เป็นวิธีการเดียวกันกับที่ Datomic ใช้กับฐานข้อมูล
Jason

1
+1 สำหรับการเปรียบเทียบที่ชัดเจนและรัดกุมระหว่างกระบวนทัศน์ หนึ่งข้อเสนอแนะคือint double(x){ return x * (++y); }เนื่องจากปัจจุบันจะยังคงเป็น 4 แม้ว่าจะยังมีผลข้างเคียงที่ไม่ได้++yตั้งใจในขณะที่จะกลับมา 6
BrainFRZ

@eelghEEz ฉันไม่แน่ใจว่ามีทางเลือกจริง ๆ ใครบ้าง หากต้องการแนะนำข้อมูลในบริบท FP (บริสุทธิ์ -) คุณ "ทำการวัด" เช่น "ที่ timestamp X อุณหภูมิคือ Y" หากมีคนถามถึงอุณหภูมิพวกเขาอาจหมายถึง X = โดยปริยาย แต่พวกเขาไม่สามารถขอให้อุณหภูมิเป็นฟังก์ชันสากลของเวลาใช่มั้ย FP เกี่ยวข้องกับสถานะที่ไม่เปลี่ยนรูปและคุณต้องสร้างสถานะที่ไม่เปลี่ยนรูป - จากแหล่งภายในและภายนอก - จากสถานะที่เปลี่ยนแปลงไม่ได้ ดัชนี timestamps ฯลฯ มีประโยชน์ แต่ orthogonal to mutability - เช่นเดียวกับ VCS คือการควบคุมเวอร์ชัน
จอห์น P

29

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

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

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

ภาคผนวก: (เกี่ยวกับการแก้ไขวิธีการติดตามค่าที่จำเป็นต้องเปลี่ยนแปลง)
พวกเขาจะถูกเก็บไว้ในโครงสร้างข้อมูลที่ไม่เปลี่ยนแปลงแน่นอน ...

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

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


2
ตกลงฉันเปลี่ยนชื่อ คำตอบของคุณดูเหมือนจะนำไปสู่ปัญหาที่เลวร้ายยิ่งกว่านั้น ถ้าฉันต้องสร้างวัตถุใหม่ทุกครั้งที่มีการเปลี่ยนแปลงสถานะฉันจะใช้เวลา CPU ทั้งหมดโดยไม่ทำอะไรนอกจากสร้างวัตถุ ฉันกำลังคิดเกี่ยวกับการเขียนโปรแกรมเกมที่นี่ซึ่งคุณมีหลายสิ่งที่เคลื่อนไหวไปมาบนหน้าจอ (และนอกจอ) ในครั้งเดียวที่จะต้องสามารถโต้ตอบกับแต่ละอื่น ๆ เอนจิ้นทั้งหมดมีการตั้งค่าเฟรม: ทุกสิ่งที่คุณจะทำคุณต้องทำในจำนวน X วินาทีของ miliseconds แน่นอนว่ามีวิธีที่ดีกว่าการรีไซเคิลวัตถุทั้งหมดอย่างต่อเนื่องใช่ไหม
Mason Wheeler

4
ความงามของมันคือความสามารถในการใช้ภาษาไม่ใช่การดำเนินการ ด้วยเทคนิคเล็กน้อยคุณสามารถมีสถานะที่ไม่น่าเชื่อถือในภาษาในขณะที่การใช้งานในความเป็นจริงการเปลี่ยนสถานะในสถานที่ ดูตัวอย่าง ST monad ของ Haskell
CesarB

4
@ Mason: จุดที่คอมไพเลอร์สามารถตัดสินใจได้ดีขึ้นว่ามันอยู่ที่ไหน (เธรด) ปลอดภัยในการเปลี่ยนสถานะในสถานที่ที่คุณสามารถทำได้
jerryjvl

ฉันคิดว่าสำหรับเกมที่คุณควรหลีกเลี่ยงไม่เปลี่ยนไม่ได้สำหรับส่วนใด ๆ ที่ความเร็วไม่สำคัญ ในขณะที่ภาษาที่ไม่เปลี่ยนรูปอาจปรับให้เหมาะสมสำหรับคุณ แต่ไม่มีอะไรจะเร็วไปกว่าการแก้ไขหน่วยความจำที่ CPU ทำงานเร็ว และถ้ามันปรากฏว่ามี 10 หรือ 20 แห่งที่คุณต้องการสิ่งที่จำเป็นฉันคิดว่าคุณควรหลีกเลี่ยงการเปลี่ยนแปลงไม่ได้เลยถ้าคุณไม่สามารถทำให้เป็นโมดูลในพื้นที่ที่แยกจากกันอย่างเช่นเมนูเกม และตรรกะของเกมโดยเฉพาะอย่างยิ่งอาจเป็นสถานที่ที่ดีในการใช้ไม่เปลี่ยนรูปเพราะฉันรู้สึกว่ามันยอดเยี่ยมสำหรับการสร้างแบบจำลองที่ซับซ้อนของระบบที่บริสุทธิ์เช่นกฎเกณฑ์ทางธุรกิจ
LegendLength

@LegendLength ที่คุณขัดแย้งกับตัวเอง
Ixx

18

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

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

คุณต้องการที่จะเห็นสิ่งนี้อธิบายในแง่ของการเขียนโปรแกรมที่จำเป็น ตกลงเรามาดูการเขียนโปรแกรมที่จำเป็นอย่างง่าย ๆ ในภาษาที่ใช้งานได้

พิจารณารหัสนี้:

int x = 1;
int y = x + 1;
x = x + y;
return x;

รหัสความจำเป็นมาตรฐานที่ลุ่มสวย ไม่ได้ทำอะไรที่น่าสนใจ แต่มันก็โอเคสำหรับภาพประกอบ ฉันคิดว่าคุณจะยอมรับว่ามีส่วนเกี่ยวข้องกับรัฐที่นี่ ค่าของตัวแปร x จะเปลี่ยนแปลงตลอดเวลา ตอนนี้ขอเปลี่ยนสัญกรณ์เล็กน้อยโดยการประดิษฐ์ไวยากรณ์ใหม่:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

ใส่วงเล็บให้ชัดเจนยิ่งขึ้นความหมาย:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

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

คุณจะพบว่ารูปแบบนี้สามารถจำลองสถานะใด ๆ แม้แต่ IO


เป็นเช่น Monad หรือไม่?
CMCDragonkai

คุณจะพิจารณาสิ่งนี้หรือไม่: A เป็นการประกาศที่ระดับ 1 B เป็นการประกาศที่ระดับ 2 โดยถือว่า A เป็นสิ่งที่จำเป็น C คือการประกาศที่ระดับ 3 โดยถือว่า B มีความจำเป็น เมื่อเราเพิ่มเลเยอร์สิ่งที่เป็นนามธรรมมันจะพิจารณาว่าภาษาที่ต่ำกว่าบนเลเยอร์สิ่งที่เป็นนามธรรมนั้นมีความจำเป็นมากกว่า
CMCDragonkai

14

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

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

กลายเป็นรหัสการทำงานนี้ (ไวยากรณ์เหมือน Scheme):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

หรือรหัส Haskellish นี้

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

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

คุณสามารถค้นหาดีกวดวิชาที่มีจำนวนมากตัวอย่างในกระดาษของจอห์นฮิวจ์ทำไมการทำงานเรื่องการเขียนโปรแกรม


13

มันเป็นวิธีที่ต่างกันในการทำสิ่งเดียวกัน

ลองพิจารณาตัวอย่างง่ายๆเช่นการเพิ่มตัวเลข 3, 5 และ 10 ลองนึกภาพการคิดเกี่ยวกับการทำเช่นนั้นโดยการเปลี่ยนค่า 3 เป็นครั้งแรกโดยการเพิ่ม 5 ลงไปจากนั้นเพิ่ม 10 เป็น "3" จากนั้นส่งออกค่าปัจจุบันของ " 3 "(18) สิ่งนี้ดูเหมือนจะไร้สาระอย่างมีเหตุผล แต่เป็นสาระสำคัญของวิธีการเขียนโปรแกรมตามความจำเป็นของรัฐมักทำ แน่นอนคุณสามารถมี "3" ที่แตกต่างกันจำนวนมากซึ่งมีค่า 3 แต่แตกต่างกัน ทั้งหมดนี้ดูแปลก ๆ เพราะเราฝังแน่นอยู่ในความคิดที่ค่อนข้างใหญ่ว่าตัวเลขนั้นไม่เปลี่ยนรูป

ทีนี้ลองคิดถึงการเพิ่ม 3, 5 และ 10 เมื่อคุณหาค่าที่ไม่เปลี่ยนรูป คุณเพิ่ม 3 และ 5 เพื่อสร้างค่าอื่น 8 จากนั้นคุณเพิ่ม 10 ให้กับค่านั้นเพื่อสร้างอีกค่าหนึ่งคือ 18

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


10

ฉันมาสายเพื่อสนทนา แต่ฉันต้องการเพิ่มบางจุดสำหรับผู้ที่ดิ้นรนกับการเขียนโปรแกรมการทำงาน

  1. ภาษาหน้าที่รักษาการปรับปรุงรัฐเดียวกันแน่นอนเป็นภาษาจำเป็น แต่พวกเขาทำมันโดยผ่านรัฐที่มีการปรับปรุงสายฟังก์ชั่นที่ตามมา นี่เป็นตัวอย่างง่ายๆของการเดินทางไปตามเส้นจำนวน รัฐของคุณคือตำแหน่งปัจจุบันของคุณ

วิธีแรกที่จำเป็น (ใน pseudocode)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

ตอนนี้วิธีการทำงาน (ใน pseudocode) ฉันกำลังพึ่งพาผู้ประกอบการที่สามอย่างมากเพราะฉันต้องการให้ผู้คนที่มีพื้นฐานที่จำเป็นต้องอ่านรหัสนี้จริง ๆ ดังนั้นหากคุณไม่ได้ใช้ผู้ประกอบการที่มาก (ฉันมักจะหลีกเลี่ยงมันในวันที่จำเป็นของฉัน) ที่นี่เป็นวิธีการทำงาน

predicate ? if-true-expression : if-false-expression

คุณสามารถโยงนิพจน์ประกอบไปด้วยการใส่นิพจน์ประกอบไปด้วยแทนที่นิพจน์เท็จ

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

ดังนั้นในใจนี่เป็นเวอร์ชั่นสำหรับการใช้งาน

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

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

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

  2. การเรียนรู้ที่จะคิดซ้ำซากไม่ยาก แต่ใช้ทั้งการฝึกฝนและชุดเครื่องมือ ส่วนเล็ก ๆ ในหนังสือ "เรียนรู้ Java" ที่พวกเขาใช้การเรียกซ้ำเพื่อคำนวณแฟคทอเรียลไม่ได้ลดลง คุณต้องการชุดเครื่องมือที่มีทักษะเช่นการทำกระบวนการวนซ้ำจากการเรียกซ้ำ (นี่คือสาเหตุที่การเรียกซ้ำหางเป็นสิ่งจำเป็นสำหรับภาษาที่ใช้งานได้), การต่อเนื่อง, ค่าคงที่, ฯลฯ คุณจะไม่เขียนโปรแกรม OO โดยไม่ต้องเรียนรู้ สำหรับการเขียนโปรแกรมการทำงาน

คำแนะนำของฉันคือทำ Little Schemer (โปรดทราบว่าฉันพูดว่า "ทำ" และไม่ใช่ "อ่าน") จากนั้นทำแบบฝึกหัดทั้งหมดใน SICP เมื่อเสร็จแล้วคุณจะมีสมองที่แตกต่างจากเมื่อคุณเริ่ม


8

อันที่จริงแล้วมันค่อนข้างง่ายที่จะมีบางสิ่งที่ดูเหมือนว่าไม่แน่นอนแม้กระทั่งในภาษาที่ไม่มีสถานะที่ไม่แน่นอน

s -> (a, s)พิจารณาฟังก์ชันที่มีประเภท การแปลจากไวยากรณ์ของ Haskell หมายถึงฟังก์ชั่นที่รับพารามิเตอร์หนึ่งตัวจากประเภท " s" และส่งคืนคู่ของค่าประเภท " a" และ " s" หากsเป็นประเภทของรัฐของเราฟังก์ชันนี้ใช้เวลาหนึ่งสถานะและส่งคืนสถานะใหม่และอาจเป็นค่าได้ (คุณสามารถส่งคืน "หน่วย" หรือที่รู้จักกัน()ซึ่งมักจะเทียบเท่ากับ " void" ใน C / C ++ เป็น " a" พิมพ์) หากคุณเชื่อมโยงฟังก์ชั่นหลาย ๆ ประเภทที่มีลักษณะเช่นนี้ (รับสถานะคืนจากฟังก์ชันหนึ่งและส่งต่อไปยังถัดไป) คุณมีสถานะ "ไม่แน่นอน" (อันที่จริงแล้วคุณอยู่ในแต่ละฟังก์ชันที่สร้างสถานะใหม่และละทิ้งเก่า) )

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

ตกลงบางทีนั่นอาจไม่ง่ายที่จะเข้าใจ :-)

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

เป็นตัวอย่างสมมติว่าคอมไพเลอร์ให้ฟังก์ชันต่อไปนี้แก่เรา:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

การแปลจากการประกาศคล้าย Haskell เหล่านี้readRefรับบางสิ่งที่คล้ายกับตัวชี้หรือหมายเลขอ้างอิงเป็นค่าชนิด " a" และสถานะปลอมและส่งคืนค่าประเภท " a" ที่ชี้ไปตามพารามิเตอร์แรกและสถานะปลอมใหม่ writeRefคล้ายกัน แต่เปลี่ยนค่าที่ชี้ไปแทน

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

(คนที่รู้จักแฮสเค็ลล์จะสังเกตเห็นว่าฉันทำให้สิ่งต่าง ๆ ง่ายขึ้นและมีรายละเอียดที่สำคัญหลายอย่างสำหรับผู้ที่ต้องการดูรายละเอียดเพิ่มเติมลองดูControl.Monad.Stateจากพระmtlและที่( ST sและ) พระและ)IOST RealWorld

คุณอาจสงสัยว่าทำไมการทำมันในลักษณะวงเวียน (แทนที่จะมีสถานะที่ไม่แน่นอนในภาษา) ประโยชน์ที่แท้จริงคือการที่คุณได้reifiedรัฐของโปรแกรม อะไรก่อนที่จะเป็นนัย (รัฐโปรแกรมของคุณเป็นระดับโลกที่ช่วยให้สำหรับสิ่งที่ต้องการดำเนินการในระยะไกล ) อยู่ในขณะนี้อย่างชัดเจน ฟังก์ชั่นที่ไม่ได้รับและส่งคืนสถานะไม่สามารถแก้ไขได้หรือได้รับอิทธิพลจากมัน พวกเขาเป็น "บริสุทธิ์" ยิ่งไปกว่านั้นคุณสามารถมีหัวข้อแยกรัฐและมีบิตของประเภทมายากล, พวกเขาสามารถใช้ในการฝังคำนวณความจำเป็นภายในหนึ่งบริสุทธิ์โดยไม่ต้องทำให้มันไม่บริสุทธิ์ (คนSTmonad ใน Haskell เป็นหนึ่งที่ใช้ตามปกติสำหรับเคล็ดลับนี้; ที่State#ฉันกล่าวถึงข้างต้นเป็นของ GHC ที่State# sใช้โดยการดำเนินงานของmonads)STและIO


7

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


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

5

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

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

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


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

แน่นอนว่าคุณสามารถทำสิ่งที่มีประโยชน์ได้ด้วยคลาส Integer และ String มันไม่เหมือนการเปลี่ยนไม่ได้หมายความว่าคุณไม่สามารถมีสถานะที่ไม่แน่นอน
Eddie

@Mason Wheeler - โดยการทำความเข้าใจว่าสิ่งหนึ่งและเป็นสถานะคือ "สิ่งของ" สองอย่างที่แตกต่างกัน สิ่งที่ Pacman จะไม่เปลี่ยนจาก A เป็นเวลา B ซึ่ง pacman จะเปลี่ยนแปลง เมื่อคุณย้ายจาก A เป็นเวลา B คุณจะได้รับการรวมกันของ pacman + state ... ซึ่งเป็น pacman เดียวกันซึ่งเป็นสถานะที่แตกต่างกัน ไม่เปลี่ยนสถานะ ... เป็นรัฐอื่น
RHSeeger

4

สำหรับแอปพลิเคชันที่มีการโต้ตอบสูงเช่นเกมการเขียนโปรแกรมปฏิกิริยาโต้ตอบเป็นเพื่อนของคุณ: หากคุณสามารถกำหนดคุณสมบัติของโลกเกมของคุณเป็นค่าที่แตกต่างกันไปตามเวลา (และ / หรือกระแสเหตุการณ์) คุณก็พร้อมแล้ว! สูตรเหล่านี้จะเป็นบางครั้งมากยิ่งขึ้นตามธรรมชาติและความตั้งใจเปิดเผยกว่ากรรมวิธีรัฐเช่นการย้ายลูกคุณโดยตรงสามารถใช้-ที่รู้จักกันดีกฎหมายx v = * t และสิ่งที่ดีกว่ากฎของเกมที่เขียนด้วยวิธีนี้ประกอบได้ดีกว่านามธรรมเชิงวัตถุ ตัวอย่างเช่นในกรณีนี้ความเร็วของลูกบอลอาจเป็นค่าที่แปรผันตามเวลาซึ่งขึ้นอยู่กับกระแสเหตุการณ์ที่ประกอบด้วยการชนของลูกบอล สำหรับการพิจารณาการออกแบบเป็นรูปธรรมมากขึ้นดูการทำเกมส์ใน Elm


4

3

นั่นคือวิธีที่ FORTRAN จะทำงานโดยไม่มีบล็อกทั่วไป: คุณต้องการเขียนวิธีการที่มีค่าที่คุณส่งผ่านและตัวแปรท้องถิ่น แค่นั้นแหละ.

การเขียนโปรแกรมเชิงวัตถุทำให้เรามีสถานะและพฤติกรรมร่วมกัน แต่เป็นแนวคิดใหม่เมื่อฉันพบครั้งแรกจาก C ++ ในปี 1994

Geez ฉันเป็นโปรแกรมเมอร์ทำงานเมื่อฉันเป็นวิศวกรเครื่องกลและฉันไม่รู้


2
ฉันไม่เห็นด้วยว่านี่เป็นสิ่งที่คุณสามารถยึดถือ OO ได้ ภาษาก่อน OO สนับสนุนการมีเพศสัมพันธ์สถานะและอัลกอริทึม OO เป็นวิธีการจัดการที่ดีกว่า
เจสันเบเกอร์

"ได้รับการสนับสนุน" - บางที OO ทำให้เป็นส่วนที่ชัดเจนของภาษา คุณสามารถห่อหุ้มและซ่อนข้อมูลใน C แต่ฉันบอกว่าภาษา OO ทำให้ง่ายขึ้นมาก
duffymo

2

ระลึกไว้เสมอ: ภาษาที่ใช้งานได้นั้นสมบูรณ์แบบ ดังนั้นงานที่มีประโยชน์ใด ๆ ที่คุณจะทำในภาษาที่จำเป็นสามารถทำได้ในภาษาที่ใช้งานได้ ในตอนท้ายของวันแม้ว่าฉันคิดว่ามีบางสิ่งบางอย่างที่จะพูดถึงวิธีการไฮบริด ภาษาอย่าง F # และ Clojure (และฉันมั่นใจว่าคนอื่น ๆ ) ส่งเสริมการออกแบบที่ไร้รัฐ แต่อนุญาตให้มีความไม่แน่นอนเมื่อจำเป็น


เพียงเพราะสองภาษาทัวริงสมบูรณ์ไม่ได้หมายความว่าพวกเขาสามารถทำงานเดียวกันได้ หมายความว่าอะไรพวกเขาสามารถทำการคำนวณเดียวกัน Brainfuck ทัวริงสมบูรณ์ แต่ฉันค่อนข้างแน่ใจว่ามันไม่สามารถสื่อสารผ่านสแต็ก TCP ได้
RHSeeger

2
แน่นอนมันสามารถ ด้วยการเข้าถึงฮาร์ดแวร์แบบเดียวกับที่บอกว่า C ไม่ได้หมายความว่ามันจะใช้ได้จริง แต่มีความเป็นไปได้อยู่ที่นั่น
เจสันเบเกอร์

2

คุณไม่สามารถใช้ภาษาที่ใช้งานได้จริงซึ่งมีประโยชน์ จะมีระดับความไม่แน่นอนที่คุณต้องรับมือเสมอ IO เป็นตัวอย่างหนึ่ง

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



-3

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

นี่คือตัวอย่าง:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

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