유형 기반 불변에 대한 함수형 프로그래밍은 무엇입니까?


9

나는 불변의 개념이 여러 프로그래밍 패러다임에 존재한다는 것을 알고 있습니다. 예를 들어, 루프 불변량 은 기능 및 절차 적 프로그래밍과 관련이 있습니다.

그러나 OOP에서 발견되는 매우 유용한 종류 중 하나는 특정 유형의 데이터가 변하지 않는 것입니다. 이것이 제목에서 "유형 기반 불변 체"라고 부르는 것입니다. 예를 들어, Fraction유형에 g numerator및 denominatorgcd가 항상 1이라는 변이 가없는 a 및가 있을 수 있습니다 (예 : 분수는 축소 된 형태 임). 데이터 유형을 자유롭게 설정하지 않고 유형의 캡슐화를 수행해야만 이것을 보장 할 수 있습니다. 그 대가로, 그것이 축소되었는지 여부를 확인할 필요가 없으므로 평등 검사와 같은 알고리즘을 단순화 할 수 있습니다.

반면에 Fraction캡슐화를 통해이 보장을 제공하지 않고 단순히 유형을 선언 하면 분수가 줄어든 것으로 가정하는 함수를 안전하게 작성할 수 없습니다. 감소되지 않은 분수를 잡는 것.

일반적으로 이러한 종류의 불변이 없으면 다음과 같은 결과가 발생할 수 있습니다.

  • 여러 곳에서 사전 조건을 확인 / 보장해야하는보다 복잡한 알고리즘
  • 이러한 반복되는 전제 조건이 동일한 기본 지식을 나타 내기 때문에 DRY 위반 (불변 값이 참이어야 함)
  • 컴파일 타임 보장이 아닌 런타임 오류를 통해 사전 조건을 시행해야 함

그래서 제 질문은 이런 종류의 불변에 대한 함수형 프로그래밍의 대답입니다. 거의 같은 것을 달성하는 기능적 관용적 방법이 있습니까? 아니면 혜택을 덜 관련시키는 기능적 프로그래밍의 일부 측면이 있습니까?


스칼라, F #, 그리고 OOP와 잘 어울리는 다른 언어들도 있지만 하스켈도 ... 기본적으로 타입을 정의 할 수있는 모든 언어와 그들의 행동이이를 지원합니다.
— AK_

@AK_ F #이 이것을 할 수 있다는 것을 알고 있습니다 (IIRC에는 약간의 후프 점핑이 필요하지만) 스칼라는 다른 크로스 패러다임 언어로 생각할 수 있습니다. Haskell이 링크를 얻을 수 있다는 것에 흥미가 있습니까? 내가 실제로 찾고 있는 것은 기능을 제공하는 특정 언어가 아니라 기능적 관념적 답변입니다. 그러나 물론 관용적 인 것에 대해 이야기하기 시작하면 오히려 애매하고 주관적 일 수 있습니다. 그래서 나는 그것을 질문에서 제외했습니다.
— Ben Aaronson

컴파일 타임에 전제 조건을 확인할 수없는 경우 생성자를 체크인하는 것이 관용적입니다. PrimeNumber수업을 고려하십시오 . 각 작업의 우선 순위에 대해 여러 중복 검사를 수행하기에는 너무 비싸지 만 컴파일 타임에 수행 할 수있는 일종의 테스트는 아닙니다. 소수에 대해 수행하고자하는 많은 연산, 곱셈, 클로저를 형성하지 않는 것, 즉 결과가 소수를 보장하지는 않습니다 (함수 프로그래밍 자체를 모르기 때문에 주석으로 게시).
— rwong

겉으로는 관련이없는 질문이지만 ... 어설 션 또는 단위 테스트가 더 중요합니까?
— rwong

@ rwong 그래, 좋은 예가 있습니다. 그러나 실제로 당신이 운전하고있는 궁극적 인 점이 100 % 명확하지 않습니다.
— Ben Aaronson

답변:


2

OCaml과 같은 일부 기능 언어에는 추상 데이터 형식 을 구현하기위한 메커니즘이 내장되어있어 일부 불변성을 적용합니다 . 그러한 메커니즘을 갖지 않는 언어는 불변을 적용하기 위해 사용자가 "카펫을 보지 않고"의존합니다.

OCaml의 추상 데이터 유형

OCaml에서 모듈 은 프로그램을 구성하는 데 사용됩니다. 모듈은 구현 과 서명을 가지며 , 후자는 모듈에 정의 된 값과 유형의 요약이며, 전자는 실제 정의를 제공합니다. 이것은 .c/.hC 프로그래머에게 친숙한 diptych와 느슨하게 비교할 수 있습니다 .

예를 들어 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 언어에는 OCaml과 동일한 추상 데이터 유형 메커니즘이 없습니다. 캡슐화를 달성하기 위해 사용자는 "카펫을 보지 않고"의존합니다.

Scheme에서는 fraction?입력을 검증 할 수있는 기회를 제공하는 값 인식 과 같은 술어를 정의하는 것이 일반적 입니다. 내 경험상 지배적 인 사용법은 사용자가 각 라이브러리 호출에서 입력을 검증하는 대신 값을 위조하는 경우 입력을 검증하도록하는 것입니다.

그러나 적용시 값을 생성하는 클로저를 반환하거나 라이브러리가 관리하는 풀의 값에 대한 참조를 반환하는 것과 같이 반환 값의 추상화를 적용하는 몇 가지 전략이 있지만 실제로는 본 적이 없습니다.


+1 또한 모든 OO 언어가 캡슐화를 강요하는 것은 아닙니다.
— Michael Shaw

5

캡슐화는 OOP와 함께 제공되는 기능이 아닙니다. 적절한 모듈화를 지원하는 모든 언어가 있습니다.

하스켈에서 대략적으로 수행하는 방법은 다음과 같습니다.

-- 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을 작성하려면 비율 함수를 사용하여 불변을 적용합니다. 데이터는 불변이므로 나중에 불변을 위반할 수 없습니다.

그러나 이로 인해 비용이 발생합니다. 더 이상 사용자가 분모 및 분자 사용과 동일한 해체 선언을 사용할 수 없습니다.


4

당신은 같은 방법을 수행 할 생성자 제약 조건을 적용하고 새로운 가치를 창출 할 때마다 그 생성자를 사용하는 데 동의합니다.

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

그러나 Karl, OOP 에서는 생성자를 사용 하기로 동의 할 필요가 없습니다 . 아 진짜?

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 #으로 코드를 작성할 수 있습니다 (관용적). 여기서 수행 한 작업을 허용하지 않습니다. 그리고 나는 불변의 적용을 담당하는 단일 클래스와 누군가가 작성한 모든 함수, 같은 불변을 강제 해야하는 특정 유형을 사용하는 모든 기능 간에는 분명한 차이가 있다고 생각합니다.
— 벤 애런 슨

@BenAaronson 변하지 않는 "강화" 와 "전파" 의 차이점에 주목하십시오 .
— rwong

1
+1. 불변의 값은 변하지 않기 때문에이 기술은 FP에서 더욱 강력합니다. 따라서 유형을 사용하여 "한 번에 모든"항목에 대해 증명할 수 있습니다. 변경 가능한 객체에서는 불가능합니다. 이제 객체에 대해 사실 인 것은 나중에 사실이 아닐 수도 있기 때문입니다. 최선을 다해 방어 상태에서 개체 상태를 다시 확인하십시오.
— Doval

@Doval 나는 그것을 보지 못했습니다. 대부분의 주요 OO 언어는 변수를 변경할 수 없도록 만드는 방법을 제외하고는 말입니다. OO에는 다음이 있습니다. 인스턴스를 만든 다음 내 함수는 불변에 부합하거나 일치하지 않는 방식으로 해당 인스턴스의 값을 변경합니다. FP에서 나는 : 인스턴스를 만든 다음 내 함수는 불변에 따르거나 일치하지 않는 방식으로 다른 값을 가진 두 번째 인스턴스를 만듭니다. 불변성이 어떻게 변하지 않는 것이 모든 유형의 인스턴스에 적합하다는 확신을 가지도록했는지 알 수 없습니다.
— Ben Aaronson

2
@ BenAaronson 불변성은 타입을 올바르게 구현했음을 증명하는 데 도움이되지 않습니다 (즉, 모든 작업은 주어진 불변량을 유지합니다). 내가 말하는 것은 값에 대한 사실을 전파 할 수 있다는 것입니다. (생성자에서 확인하여) 유형의 일부 조건 (예 :이 숫자는 짝수)을 인코딩하고 생성 된 값은 원래 값이 조건을 만족했음을 증명합니다. 변경 가능한 객체를 사용하면 현재 상태를 확인하고 결과를 부울로 유지합니다. 그 부울은 객체가 변경되지 않아 조건이 거짓이되는 한에만 유효합니다.
— Doval
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.