Apa jawaban pemrograman fungsional untuk invarian berbasis tipe?

9

Saya sadar bahwa konsep invarian ada di berbagai paradigma pemrograman. Sebagai contoh, invarian loop relevan dalam pemrograman OO, fungsional dan prosedural.

Namun, satu jenis yang sangat berguna yang ditemukan di OOP adalah invarian data tipe tertentu. Inilah yang saya sebut "invarian berbasis tipe" dalam judul. Sebagai contoh, suatu Fractiontipe mungkin memiliki a numeratordan denominator, dengan invarian bahwa gcd mereka selalu 1 (yaitu fraksi dalam bentuk tereduksi). Saya hanya bisa menjamin ini dengan memiliki semacam enkapsulasi jenis, tidak membiarkan datanya diatur secara bebas. Sebagai imbalannya, saya tidak pernah harus memeriksa apakah itu berkurang, jadi saya dapat menyederhanakan algoritma seperti pemeriksaan kesetaraan.

Di sisi lain, jika saya hanya mendeklarasikan Fractiontipe tanpa memberikan jaminan ini melalui enkapsulasi, saya tidak dapat dengan aman menulis fungsi apa pun pada tipe ini yang mengasumsikan bahwa fraksi berkurang, karena di masa depan orang lain dapat datang dan menambahkan cara untuk mendapatkan fraksi yang tidak berkurang.

Secara umum, kurangnya invarian semacam ini dapat menyebabkan:

  • Algoritma yang lebih kompleks karena prasyarat perlu diperiksa / dipastikan di banyak tempat
  • Pelanggaran KERING karena pra-kondisi berulang ini mewakili pengetahuan dasar yang sama (bahwa invarian harus benar)
  • Harus menerapkan pra-kondisi melalui kegagalan runtime daripada jaminan waktu kompilasi

Jadi pertanyaan saya adalah apa jawaban pemrograman fungsional untuk jenis invarian ini. Apakah ada cara fungsional-idiomatis untuk mencapai hal yang kurang lebih sama? Atau ada beberapa aspek pemrograman fungsional yang membuat manfaatnya kurang relevan?

Ben Aaronson
sumber
banyak bahasa fungsional dapat melakukan ini secara sepele ... Scala, F #, dan bahasa lain yang bermain bagus dengan OOP, Tapi Haskell juga ... pada dasarnya setiap bahasa yang memungkinkan Anda untuk menentukan jenis dan perilakunya mendukung hal ini.
AK_
@AK_ Saya sadar F # dapat melakukan ini (meskipun IIRC membutuhkan beberapa simpai-lompatan kecil) dan menebak Scala bisa sebagai bahasa lintas-paradigma lain. Menarik bahwa Haskell dapat melakukannya- mendapat tautan? Yang benar-benar saya cari adalah jawaban fungsional-idiomatik , bukan bahasa tertentu yang menawarkan fitur. Tapi tentu saja hal-hal bisa menjadi agak kabur dan subyektif setelah Anda mulai berbicara tentang apa yang idiomatis, itulah sebabnya saya tidak mengikutinya.
Ben Aaronson
Untuk kasus-kasus di mana prasyarat tidak dapat diperiksa pada waktu kompilasi, adalah idiomatis untuk memeriksa konstruktor. Pertimbangkan PrimeNumberkelas. Akan terlalu mahal untuk melakukan beberapa pemeriksaan berlebih untuk primality untuk setiap operasi, tetapi ini bukan semacam tes yang dapat dilakukan pada waktu kompilasi. (Banyak operasi yang ingin Anda lakukan pada bilangan prima, katakanlah perkalian, jangan membentuk penutupan , yaitu hasil mungkin tidak dijamin prima. (Posting sebagai komentar karena saya sendiri tidak tahu pemrograman fungsional.)
rwong
Sebuah pertanyaan yang tampaknya tidak berhubungan, tetapi ... Apakah tes menegaskan atau unit lebih penting?
rwong
@ rwong Ya, beberapa contoh bagus di sana. Saya sebenarnya tidak 100% jelas pada titik apa Anda mengemudi.
Ben Aaronson

Jawaban:

2

Beberapa bahasa fungsional seperti OCaml memiliki mekanisme built-in untuk mengimplementasikan tipe data abstrak sehingga memberlakukan beberapa invarian . Bahasa yang tidak memiliki mekanisme seperti itu bergantung pada pengguna "tidak melihat di bawah karpet" untuk menegakkan invarian.

Tipe data abstrak dalam OCaml

Di OCaml, modul digunakan untuk menyusun program. Modul memiliki implementasi dan tanda tangan , yang terakhir adalah semacam ringkasan dari nilai dan tipe yang didefinisikan dalam modul, sementara yang pertama memberikan definisi aktual. Ini dapat secara longgar dibandingkan dengan diptych yang .c/.hakrab dengan programmer C.

Sebagai contoh, kita dapat mengimplementasikan Fractionmodul seperti ini:

# 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

Definisi ini sekarang dapat digunakan seperti ini:

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

Siapa pun dapat menghasilkan nilai fraksi tipe secara langsung, melewati jaring pengaman bawaan Fraction.make:

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

Untuk mencegah hal ini, dimungkinkan untuk menyembunyikan definisi konkret dari tipe Fraction.tseperti itu:

# 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

Satu-satunya cara untuk membuat AbstractFraction.tadalah menggunakan AbstractFraction.makefungsi.

Tipe data abstrak dalam Skema

Bahasa Skema tidak memiliki mekanisme yang sama dengan tipe data abstrak seperti OCaml. Itu bergantung pada pengguna "tidak melihat di bawah karpet" untuk mencapai enkapsulasi.

Dalam Skema, sudah lazim untuk mendefinisikan predikat seperti fraction?mengenali nilai yang memberi kesempatan untuk memvalidasi input. Dalam pengalaman saya, penggunaan yang dominan adalah membiarkan pengguna memvalidasi inputnya, jika dia memalsukan suatu nilai, dan bukannya memvalidasi input dalam setiap panggilan pustaka.

Namun ada beberapa strategi untuk menegakkan abstraksi nilai yang dikembalikan, seperti mengembalikan penutupan yang menghasilkan nilai ketika diterapkan atau mengembalikan referensi ke nilai dalam kumpulan yang dikelola oleh perpustakaan - tetapi saya tidak pernah melihat satu pun dari mereka dalam praktik.

Michael Le Barbier Grünewald
sumber
+1 Juga patut disebutkan bahwa tidak semua bahasa OO memberlakukan enkapsulasi.
Michael Shaw
5

Enkapsulasi bukan fitur yang disertakan dengan OOP. Bahasa apa pun yang mendukung modularisasi yang tepat memilikinya.

Berikut kira-kira cara Anda melakukannya di 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)

Sekarang, untuk membuat Rasional, Anda menggunakan fungsi rasio, yang memberlakukan invarian. Karena data tidak dapat diubah, Anda nantinya tidak dapat melanggar invarian.

Ini memang membuat Anda harus membayar sesuatu: pengguna tidak lagi dapat menggunakan deklarasi dekonstruksi yang sama seperti penggunaan penyebut dan pembilang.

Sebastian Redl
sumber
4

Anda melakukannya dengan cara yang sama: membuat konstruktor yang memberlakukan kendala, dan setuju untuk menggunakan konstruktor itu setiap kali Anda membuat nilai baru.

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

Tetapi Karl, di OOP Anda tidak harus setuju untuk menggunakan konstruktor. Oh benarkah?

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

Faktanya, peluang untuk penyalahgunaan semacam ini lebih sedikit di FP. Anda harus meletakkan konstruktor sebagai yang terakhir, karena kekekalan. Saya berharap orang-orang akan berhenti memikirkan enkapsulasi sebagai semacam perlindungan terhadap kolega yang tidak kompeten, atau sebagai penghindaran kebutuhan untuk mengomunikasikan kendala. Itu tidak melakukan itu. Itu hanya membatasi tempat Anda harus memeriksa. Pemrogram FP yang baik juga menggunakan enkapsulasi. Itu hanya datang dalam bentuk mengkomunikasikan beberapa fungsi yang disukai untuk membuat jenis modifikasi tertentu.

Karl Bielefeldt
sumber
Yah itu mungkin (dan idiomatik) untuk menulis kode dalam C #, misalnya, yang tidak memungkinkan apa yang Anda lakukan di sana. Dan saya pikir ada perbedaan yang cukup jelas antara satu kelas yang bertanggung jawab untuk menegakkan invarian dan setiap fungsi yang ditulis oleh siapa pun, di mana saja yang menggunakan jenis tertentu harus menegakkan invarian yang sama.
Ben Aaronson
@BenAaronson Perhatikan perbedaan antara "menegakkan" dan "menyebarkan" invarian.
rwong
1
+1. Teknik ini bahkan lebih kuat di FP karena nilai yang tidak berubah tidak berubah; sehingga Anda dapat membuktikan hal-hal tentang mereka "sekali dan untuk semua" menggunakan tipe. Ini tidak mungkin dengan objek yang bisa berubah karena apa yang benar untuk mereka sekarang mungkin tidak benar nanti; yang terbaik yang dapat Anda lakukan dengan memeriksa ulang keadaan objek secara defensif.
Doval
@Doval saya tidak melihatnya. Mengesampingkan sebagian besar (?) Bahasa OO utama memiliki cara membuat variabel berubah. Di OO ​​saya punya: Buat instance, maka fungsi saya mengubah nilai instance itu dengan cara yang mungkin atau tidak sesuai dengan invarian. Di FP saya punya: Buat instance, maka fungsi saya membuat instance kedua dengan nilai yang berbeda dengan cara yang mungkin atau tidak sesuai dengan invarian. Saya tidak melihat bagaimana ketidakberdayaan telah membantu saya merasa lebih percaya diri bahwa invarian saya cocok untuk semua contoh tipe
Ben Aaronson
2
@BenAaronson Immutability tidak akan membantu Anda membuktikan bahwa Anda telah menerapkan tipe Anda dengan benar (yaitu semua operasi mempertahankan beberapa invarian tertentu.) Yang saya katakan adalah memungkinkan Anda untuk menyebarkan fakta tentang nilai. Anda menyandikan beberapa kondisi (mis. Angka ini genap) dalam tipe (dengan memeriksanya di konstruktor) dan nilai yang dihasilkan adalah bukti bahwa nilai asli memenuhi syarat. Dengan objek yang dapat diubah, Anda memeriksa status saat ini dan menyimpan hasilnya dalam boolean. Itu boolean hanya baik selama objek tidak bermutasi sehingga kondisinya salah.
Doval