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


9

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

그러나 OOP에서 발견되는 매우 유용한 종류 중 하나는 특정 유형의 데이터가 변하지 않는 것입니다. 이것이 제목에서 "유형 기반 불변 체"라고 부르는 것입니다. 예를 들어, Fraction유형에 g numeratordenominatorgcd가 항상 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.tAbstractFraction.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.