태그 보관물: solid

solid

인터페이스 (OOP)의 시맨틱 계약이 기능 서명 (FP)보다 유익한 정보입니까? 인터페이스에 도달하게됩니다. 이것은 나에게

일부는 SOLID 원칙을 최대한 활용하면 기능적 프로그래밍을하게 된다고 말합니다 . 나는이 기사에 동의하지만 인터페이스 / 객체에서 함수 / 클로저로의 전환에서 일부 의미가 손실된다고 생각하며 함수 프로그래밍이 손실을 완화시키는 방법을 알고 싶습니다.

기사에서 :

또한 ISP (Interface Segregation Principle)를 엄격하게 적용하면 헤더 인터페이스보다 역할 인터페이스를 선호해야합니다.

더 작고 더 작은 인터페이스를 향한 설계를 계속 추진하면 궁극적으로 단일 방법의 인터페이스 인 궁극적 인 역할 인터페이스에 도달하게됩니다. 이것은 나에게 많이 일어난다. 예를 들면 다음과 같습니다.

public interface IMessageQuery
{
    string Read(int id);
}

에 의존하는 경우 암시 적 계약의IMessageQuery 일부는 호출 이 주어진 ID를 가진 메시지를 검색하고 반환 한다는 것입니다.Read(id)

이것을 동등한 기능적 서명에 의존하는 것과 비교하십시오 int -> string. 추가 신호가 없으면이 함수는 간단 할 수 있습니다 ToString(). 당신이 구현되면 IMessageQuery.Read(int id)ToString()나는 의도적으로 파괴되는 당신을 비난 할 수있다!

그렇다면 잘 알려진 인터페이스의 의미를 보존하기 위해 기능 프로그래머는 무엇을 할 수 있습니까? 예를 들어, 단일 멤버로 레코드 유형을 작성하는 것이 일반적입니까?

type MessageQuery = {
    Read: int -> string
}



답변

Telastyn이 말했듯이 함수의 정적 정의를 비교하면 다음과 같습니다.

public string Read(int id) { /*...*/ }

let read (id:int) = //...

OOP에서 FP로가는 것을 잃어버린 것은 없습니다.

그러나 함수와 인터페이스는 정적 정의에서만 참조되지 않기 때문에 이것은 이야기의 일부일뿐입니다. 그들은 또한 전달됩니다 . 우리 MessageQuery가 다른 코드 조각에 의해 읽혀 졌다고 가정 해 봅시다 MessageProcessor. 그리고 우리는 :

public void ProcessMessage(int messageId, IMessageQuery messageReader) { /*...*/ }

이제 메서드 이름 이나 매개 변수를 직접 볼 수는 없지만 IDE를 통해 쉽게 얻을 수 있습니다. 보다 일반적으로, 우리는 int에서 string까지의 함수를 가진 메소드를 가진 인터페이스가 아니라이 함수와 관련된 매개 변수 이름 메타 데이터를 유지한다는 것을 의미 합니다.IMessageQuery.Readint idIMessageQueryid

반면에 기능 버전의 경우 다음이 있습니다.

let read (id:int) (messageReader : int -> string) = // ...

그래서 우리는 무엇을 유지하고 잃었습니까? 글쎄, 우리는 여전히 name 매개 변수를 가지고 messageReader있는데, 이것은 아마도 형식 이름 (과 동등한 IMessageQuery)을 불필요하게 만듭니다. 그러나 이제 id함수에서 매개 변수 이름 을 잃어 버렸습니다 .


이 문제를 해결하는 데는 두 가지 주요 방법이 있습니다.

  • 첫째, 그 서명을 읽음으로써 이미 무슨 일이 벌어 질지 추측 할 수 있습니다. 함수를 짧고 단순하며 응집력있게 유지하고 이름을 잘 지정하면이 정보를 쉽게 이해하고 찾을 수 있습니다. 실제 함수 자체를 읽은 후에는 훨씬 더 간단해질 것입니다.

  • 둘째, 기본형을 감싸는 작은 유형을 만들기 위해 많은 기능적 언어에서 관용적 디자인으로 간주됩니다 . 이 경우 반대의 상황이 발생합니다. 유형 이름을 매개 변수 이름 ( IMessageQueryto messageReader)으로 바꾸는 대신 매개 변수 이름을 유형 이름으로 바꿀 수 있습니다. 예를 들어, 다음 int과 같은 유형으로 랩핑 될 수 있습니다 Id.

    type Id = Id of int
    

    이제 우리의 read서명은 다음과 같습니다.

    let read (id:int) (messageReader : Id -> string) = // ...
    

    우리가 이전에했던 것만 큼 유익합니다.

    참고로, 이것은 우리가 OOP에서 가지고 있던 컴파일러 보호 기능을 제공합니다. OOP의 버전이 보장 반면 우리는 특별히했다 IMessageQuery단지 오래된보다는 int -> string기능, 여기에 우리는 우리가 복용하고 있다는 비슷한 (하지만 다른) 보호가 Id -> string단지 오래된보다는를 int -> string.


이러한 기술이 인터페이스에서 전체 정보를 사용할 수있는 것만큼이나 유익하고 유익 할 것이라고 100 % 확신하며 말하기를 꺼려하지만, 위의 예에서 대부분의 경우, 아마 좋은 일을 할 수 있습니다.


답변

FP를 수행 할 때 더 구체적인 의미 유형을 사용하는 경향이 있습니다.

예를 들어, 나를위한 방법은 다음과 같습니다.

read: MessageId -> Message

객체 지향보다이 통신하는 꽤 많은 (/ 자바) 스타일 ThingDoer.doThing()스타일


답변

그렇다면 잘 알려진 인터페이스의 의미를 보존하기 위해 기능 프로그래머는 무엇을 할 수 있습니까?

잘 명명 된 기능을 사용하십시오.

IMessageQuery::Read: int -> string단순히 ReadMessageQuery: int -> string또는 이와 비슷한 것이됩니다 .

주목할 점은 이름은 가장 느슨한 의미에서 계약일뿐입니다. 그들은 당신과 다른 프로그래머가 이름에서 같은 의미를 추론하고 순종 할 때만 작동합니다. 이 때문에 내재 된 동작을 나타내는 모든 이름을 사용할 수 있습니다 . OO와 함수형 프로그래밍은 이름이 약간 다른 위치와 형태가 약간 다르지만 기능은 동일합니다.

인터페이스 (OOP)의 시맨틱 계약이 기능 서명 (FP)보다 유익한 정보입니까?

이 예에는 없습니다. 위에서 설명한 것처럼 단일 함수를 가진 단일 클래스 / 인터페이스는 비슷한 이름의 독립형 함수보다 의미있는 정보가 아닙니다.

클래스에서 둘 이상의 함수 / 필드 / 프로퍼티를 얻으면 관계를 볼 수 있으므로 더 많은 정보를 유추 할 수 있습니다. 네임 스페이스 또는 모듈로 구성된 동일하거나 유사한 매개 변수 또는 독립형 함수를 취하는 독립형 함수보다 유익한 정보인지는 논쟁의 여지가 있습니다.

개인적으로, 나는 좀 더 복잡한 예에서도 OO가 훨씬 더 유익하다고 생각하지 않습니다.


답변

하나의 함수가 ‘의미 적 계약’을 가질 수 없다는 데 동의하지 않습니다. 다음에 대한 법률을 고려하십시오 foldr.

foldr f z nil = z
foldr f z (singleton x) = f x z
foldr f z (xn <> ys) = foldr f (foldr f z ys) xn

그 의미가 의미가 아니거나 계약이 아닌 것은 무엇입니까? ‘폴더’에 대한 유형을 정의 할 필요는 없습니다. 특히 foldr해당 법률에 따라 고유하게 결정 되기 때문 입니다. 당신은 그것이 무엇을할지 정확히 알고 있습니다.

어떤 유형의 기능을 사용하려면 동일한 작업을 수행 할 수 있습니다.

-- The first argument `f` must satisfy for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
sort :: forall 'a. (a -> a -> bool.t) -> list.t a -> list.t a;

동일한 계약이 여러 번 필요한 경우에만 해당 유형의 이름을 지정하고 캡처하면됩니다.

-- Type of functions `f` satisfying, for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
type comparison.t 'a = a -> a -> bool.t;

타입 체커는 타입에 할당 된 의미를 강제하지 않을 것이므로 모든 계약에 대해 새로운 타입을 만드는 것은 단순한 보일러 플레이트입니다.


답변

거의 모든 정적으로 유형화 된 기능 언어는 의미 적 의도를 명시 적으로 선언해야하는 방식으로 기본 유형의 별명을 지정할 수 있습니다. 다른 답변 중 일부는 예를 제공했습니다. 실제로, 숙련 된 기능적 프로그래머는 이러한 랩퍼 유형을 사용해야하는 이유 가 매우 작습니다 . 컴포저 빌 러티와 재사용 성이 손상되기 때문입니다.

예를 들어, 클라이언트가 목록에 의해 지원되는 메시지 쿼리의 구현을 원한다고 가정합니다. Haskell에서 구현은 다음과 같이 간단 할 수 있습니다.

messages = ["Message 0", "Message 1", "Message 2"]
messageQuery = (messages !!)

newtype Message = Message String이것을 사용 하는 것은 훨씬 간단하지 않으며이 구현은 다음과 같이 보입니다.

messages = map Message ["Message 0", "Message 1", "Message 2"]
messageQuery (Id index) = messages !! index

그것은 큰 일처럼 보이지 않을 수도 있지만, 어디서나 유형 변환을 수행 하거나 위의 모든 것이있는 코드에서 경계 레이어를 설정 Int -> String한 다음 Id -> Message아래 레이어로 전달하도록 변환해야 합니다. 국제화를 추가하거나 모든 대문자로 형식화하거나 로깅 컨텍스트 또는 기타를 추가하고 싶다고 가정 해보십시오. 이러한 작업은 모두로 구성하기가 간단 Int -> String하고으로 성가시다 Id -> Message. 증가 된 유형 제한이 바람직한 경우는 없지만 성가심이 더 가치가 있습니다.

래퍼 대신 유형 동의어 를 사용할 수 있습니다 ( ) type대신 Haskell에서 사용 newtype하는 것이 훨씬 일반적이며 변환이 필요하지 않지만 OOP 버전과 같은 정적 유형 보장은 제공하지 않습니다. 약간의 캡슐화. 타입 래퍼 는 주로 클라이언트가 값을 조작하지 않고 저장하고 다시 전달할 것으로 예상되는 곳에서 주로 사용됩니다. 예를 들어 파일 핸들입니다.

클라이언트가 “복잡”하는 것을 막을 수있는 것은 없습니다. 당신은 모든 단일 클라이언트에 대해 뛰어 넘기 위해 농구대를 만들고 있습니다. 공통 단위 테스트를위한 간단한 모의는 종종 생산에 적합하지 않은 이상한 행동을 요구합니다 . 인터페이스는 가능한 한 신경 쓰지 않도록 작성해야합니다.


답변