การเขียนโปรแกรมการทำงานคืออะไรคำตอบให้กับค่าคงที่ประเภทตาม?


9

ฉันรู้ว่าแนวคิดของค่าคงที่มีอยู่ในกระบวนทัศน์การเขียนโปรแกรมหลายรายการ ตัวอย่างเช่นค่าคงที่ลูปมีความเกี่ยวข้องใน OO การเขียนโปรแกรมเชิงฟังก์ชันและโพรซีเดอร์

อย่างไรก็ตามชนิดหนึ่งที่มีประโยชน์มากที่พบใน OOP คือค่าคงที่ของข้อมูลประเภทใดประเภทหนึ่ง นี่คือสิ่งที่ฉันเรียกว่า "ค่าคงที่ประเภทตาม" ในชื่อ ตัวอย่างเช่นFractionประเภทอาจมีnumeratorและdenominatorด้วยค่าคงที่ gcd ของพวกเขาเสมอ 1 (เช่นเศษส่วนอยู่ในรูปแบบลดลง) ฉันสามารถรับประกันได้เพียงแค่มีการห่อหุ้มบางประเภทไม่ให้ข้อมูลถูกตั้งค่าอย่างอิสระ ในทางกลับกันฉันไม่ต้องตรวจสอบว่ามันลดลงหรือไม่ดังนั้นฉันจึงสามารถลดความซับซ้อนของอัลกอริทึมเช่นการตรวจสอบความเท่าเทียมกันได้

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

โดยทั่วไปการขาดค่าคงที่ประเภทนี้อาจนำไปสู่:

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

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


ภาษาที่ใช้งานได้หลายอย่างสามารถทำสิ่งนี้ได้เล็กน้อย ... Scala, F # และภาษาอื่น ๆ ที่เล่นกับ OOP ได้ดี แต่ Haskell ก็เช่นกัน ... โดยพื้นฐานแล้วภาษาใด ๆ ที่อนุญาตให้คุณกำหนดประเภทและพฤติกรรมของพวกเขารองรับสิ่งนี้
AK_

@AK_ ฉันรู้ว่า F # สามารถทำสิ่งนี้ได้ (แม้ว่า IIRC จะต้องใช้การกระโดดแบบห่วงเล็กน้อย) และเดาว่าสกาล่าอาจเป็นภาษาข้ามกระบวนทัศน์อีกภาษาหนึ่ง น่าสนใจที่ Haskell สามารถทำได้ - มีลิงค์? สิ่งที่ฉันกำลังมองหาคือคำตอบที่ใช้งานง่ายมากกว่าภาษาเฉพาะที่มีคุณสมบัติ แต่แน่นอนว่าสิ่งต่าง ๆ จะค่อนข้างคลุมเครือและเป็นอัตวิสัยเมื่อคุณเริ่มพูดถึงสิ่งที่เป็นสำนวนซึ่งเป็นสาเหตุที่ฉันทิ้งมันไว้จากคำถาม
Ben Aaronson

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

คำถามที่ดูเหมือนไม่เกี่ยวข้อง แต่ ... การยืนยันหรือการทดสอบหน่วยสำคัญกว่าหรือไม่

@rwong Yeah ตัวอย่างที่ดีที่นั่น จริง ๆ แล้วฉันไม่ชัดเจน 100% ว่าคุณกำลังขับรถไปที่จุดไหน
Ben Aaronson

คำตอบ:


2

บางภาษาทำงานเช่น OCaml ได้ในตัวกลไกที่จะใช้ชนิดข้อมูลนามธรรมดังนั้นการบังคับใช้บางส่วนคงที่ ภาษาที่ไม่มีกลไกดังกล่าวขึ้นอยู่กับผู้ใช้“ ไม่ได้ดูใต้พรม” เพื่อบังคับใช้ค่าคงที่

ชนิดข้อมูลนามธรรมใน OCaml

ใน OCaml โมดูลจะใช้เพื่อจัดโครงสร้างโปรแกรม โมดูลมีการนำไปใช้และลายเซ็นซึ่งเป็นชนิดของการสรุปค่าและประเภทที่กำหนดไว้ในโมดูลในขณะที่โมดูลเดิมให้นิยามที่แท้จริง สิ่งนี้สามารถเปรียบเทียบได้อย่างหลวม ๆ กับ diptych ที่.c/.hคุ้นเคยกับโปรแกรมเมอร์ C

ตัวอย่างเช่นเราสามารถใช้Fractionโมดูลดังนี้:

# module Fraction = struct
  type t = Fraction of int * int
  let rec gcd a b =
    match a mod b with
    | 0 -> b
    | r -> gcd b r

  let make a b =
   if b = 0 then
     invalid_arg "Fraction.make"
   else let d = gcd (abs a) (abs b) in
     Fraction(a/d, b/d)

  let to_string (Fraction(a,b)) =
    Printf.sprintf "Fraction(%d,%d)" a b

  let add (Fraction(a1,b1)) (Fraction(a2,b2)) =
    make (a1*b2 + a2*b1) (b1*b2)

  let mult (Fraction(a1,b1)) (Fraction(a2,b2)) =
    make (a1*a2) (b1*b2)
end;;

module Fraction :
  sig
    type t = Fraction of int * int
    val gcd : int -> int -> int
    val make : int -> int -> t
    val to_string : t -> string
    val add : t -> t -> t
    val mult : t -> t -> t
  end

คำจำกัดความนี้สามารถใช้ได้ดังนี้:

# Fraction.add (Fraction.make 8 6) (Fraction.make 14 21);;
- : Fraction.t = Fraction.Fraction (2, 1)

ทุกคนสามารถสร้างคุณค่าของเศษส่วนประเภทได้โดยตรงโดยไม่ต้องผ่านเครือข่ายความปลอดภัยภายในFraction.make:

# Fraction.Fraction(0,0);;
- : Fraction.t = Fraction.Fraction (0, 0)

เพื่อป้องกันสิ่งนี้คุณสามารถซ่อนนิยามที่เป็นรูปธรรมของประเภทFraction.tดังนี้:

# module AbstractFraction : sig
  type t
  val make : int -> int -> t
  val to_string : t -> string
  val add : t -> t -> t
  val mult : t -> t -> t
end = Fraction;;

module AbstractFraction :
sig
  type t
  val make : int -> int -> t
  val to_string : t -> string
  val add : t -> t -> t
  val mult : t -> t -> t
end

วิธีเดียวที่จะสร้างAbstractFraction.tคือการใช้AbstractFraction.makeฟังก์ชั่น

ชนิดข้อมูลนามธรรมใน Scheme

ภาษา Scheme ไม่มีกลไกข้อมูลนามธรรมชนิดเดียวกับที่ OCaml ทำ มันขึ้นอยู่กับผู้ใช้“ ไม่ได้ดูใต้พรม” เพื่อให้ได้การห่อหุ้ม

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

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


+1 เป็นมูลค่าการกล่าวขวัญว่าไม่ใช่ทุกภาษาของ OO ที่บังคับใช้การห่อหุ้ม
Michael Shaw

5

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

นี่เป็นวิธีที่คุณทำใน Haskell:

-- Rational.hs
module Rational (
    -- This is the export list. Functions not in this list aren't visible to importers.
    Rational, -- Exports the data type, but not its constructor.
    ratio,
    numerator,
    denominator
    ) where

data Rational = Rational Int Int

-- This is the function we provide for users to create rationals
ratio :: Int -> Int -> Rational
ratio num den = let (num', den') = reduce num den
                 in Rational num' den'

-- These are the member accessors
numerator :: Rational -> Int
numerator (Rational num _) = num

denominator :: Rational -> Int
denominator (Rational _ den) = den

reduce :: Int -> Int -> (Int, Int)
reduce a b = let g = gcd a b
             in (a `div` g, b `div` g)

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

สิ่งนี้มีค่าใช้จ่ายสำหรับคุณบางอย่าง: ผู้ใช้ไม่สามารถใช้ deconstructing declaration แบบเดียวกับการใช้ตัวหารและตัวเศษ


4

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

multiply lhs rhs = ReducedFraction (lhs.num * rhs.num) (lhs.denom * rhs.denom)

แต่ Karl ใน OOP คุณไม่จำเป็นต้องตกลงที่จะใช้ Constructor จริงๆเหรอ?

class Fraction:
  ...
  Fraction multiply(Fraction lhs, Fraction rhs):
    Fraction result = lhs.clone()
    result.num *= rhs.num
    result.denom *= rhs.denom
    return result

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


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

@BenAaronson สังเกตเห็นความแตกต่างระหว่าง"การบังคับใช้"และ"การเผยแพร่"ค่าคงที่

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

@Doval ฉันไม่เห็นมัน วางไว้ว่าส่วนใหญ่ (?) ภาษา OO ที่สำคัญมีวิธีการทำให้ตัวแปรไม่เปลี่ยนรูป ใน OO ฉันมี: สร้างอินสแตนซ์จากนั้นฟังก์ชั่นของฉันจะเปลี่ยนค่าของอินสแตนซ์นั้นในแบบที่อาจจะหรืออาจไม่สอดคล้องกับค่าคงที่ ใน FP ฉันมี: สร้างอินสแตนซ์จากนั้นฟังก์ชั่นของฉันจะสร้างอินสแตนซ์ที่สองที่มีค่าแตกต่างกันในแบบที่อาจหรือไม่สอดคล้องกับค่าคงที่ ฉันไม่เห็นว่าความผันแปรไม่ได้ช่วยให้ฉันรู้สึกมั่นใจมากขึ้นว่าค่าคงที่ของฉันสอดคล้องกับทุกประเภท
เบ็นอารอนสัน

2
@BenAaronson Immutability จะไม่ช่วยให้คุณพิสูจน์ได้ว่าคุณใช้ประเภทของคุณอย่างถูกต้อง (เช่นการดำเนินการทั้งหมดรักษาค่าคงที่ที่กำหนดไว้) สิ่งที่ฉันพูดคือการอนุญาตให้คุณเผยแพร่ข้อเท็จจริงเกี่ยวกับค่านิยม คุณเข้ารหัสเงื่อนไขบางอย่าง (เช่นหมายเลขนี้คือเลขคู่) ในประเภท (โดยการตรวจสอบในตัวสร้าง) และค่าที่ผลิตนั้นเป็นเครื่องพิสูจน์ว่าค่าเดิมเป็นไปตามเงื่อนไข ด้วยวัตถุที่ไม่แน่นอนคุณจะตรวจสอบสถานะปัจจุบันและเก็บผลลัพธ์ไว้ในบูลีน บูลีนนั้นดีสำหรับตราบใดที่วัตถุไม่ได้กลายพันธุ์เพื่อให้เงื่อนไขเป็นเท็จ
Doval
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.