오류 없는 C# 1부: 어쩌면 모나드

25639 단어 functionalmonadcsharp
대부분의 사람들은 내가 나를 전문 C# 개발자이자 전문 기능 프로그래머라고 생각한다는 사실에 놀랄 것입니다. C#은 주로 객체 지향 언어입니다. 구문은 함수형 프로그래밍에 최적화되어 있지 않습니다. 이것은 전적으로 사실이지만 Microsoft가 프로그래밍의 새로운 모범 사례를 지원하는 데 뒤처져 있다고 해서 꼭 그래야 하는 것은 아닙니다. 이 시리즈에서는 런타임 오류 없이 실행이 보장되는 C# 코드를 빌드하고 사용하는 방법을 안내합니다.

제거할 오류의 첫 번째 클래스는 NullReferenceException 입니다. 강조하고 싶은 것은 주로 객체 지향 언어를 사용한다고 해서 지루하고 수동적인 null 검사와 가끔 발생하는 NullReferenceException s와 함께 살아야 한다는 의미는 아닙니다. 특히 언어가 익명 함수를 지원하고 실제로 C#이 지원하는 경우 그럴 필요가 없습니다. 아마도 모나드에서 nullable 값을 래핑하여 NullReferenceException 가능성을 제거할 수 있습니다.

함수형 언어에서 "maybe"는 값이 있는 "some"과 값이 없는 "none"의 두 가지 가능성에 대한 인터페이스입니다. 거기에서 시작합시다.

아마.cs

public interface IMaybe<T>{}

이제 Some 의 구성을 제어하는 ​​방법을 결정해야 합니다. 종종 C#은 NullArgumentException 던지기를 권장합니다. 그러나 다른 옵션이 있고 그렇게 하는 한 이는 좋은 방법이 아닙니다. 여기서는 internal 키워드를 사용하여 구현을 숨기는 객체 지향 모범 사례와 유효하지 않은 상태를 표현할 수 없도록 만드는 기능 모범 사례를 모두 따릅니다.

일부.cs

public class Some<T> : IMaybe<T>
{ 
    private T member; 
    internal Some(T member) { this.member = member; }
}

없음.cs

public class None<T> : IMaybe<T>
{
}

이제 다음 정적 클래스를 Maybe.cs에 추가할 수 있습니다. 그를 널 검사의 DRY principle이라고 생각할 수 있습니다. 여기서 한 번만 수행하고 코드의 다른 곳에서는 null 검사를 반복하지 않습니다.

아마.cs

public interface IMaybe<T>
{
}

public static class Maybe
{
    public IMaybe<T> Factory(T member) 
      => member == null ? new None<T>() ? new Some<T>(member);
}

축하합니다. 마지막으로 null 검사를 작성했습니다.

우리의 "아마도"인터페이스는 존재하지 않는 객체와 상호작용을 시도하는 것을 방지할 수 있습니다. 이 상호 작용을 허용하는 메서드를 추가해 보겠습니다.

아마.cs

public interface IMaybe<T>
{ 
    /// <summary> 
    /// applies `func` if and only if object exists 
    /// </summary> 
    /// <returns>a new Some of the result, or None if this is None</returns> 
    IMaybe<TNext> Map<TNext>(Func<T, TNext> func);
}

public static class Maybe
{ 
    public IMaybe<T> Factory(T member) 
      => member == null ? new None<T>() ? new Some<T>(member);
}

일부.cs

public class Some<T> : IMaybe<T>
{ 
    private T member; 
    internal Some(T member) 
    { 
        this.member = member; 
    } 

    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
      => Maybe.Factory(func(member));
}

없음.cs

public class None<T> : IMaybe<T>
{ 
    IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
      => new None<TNext>();
}

이제 우리는 모나드가 우리를 보호하는 방법을 볼 수 있습니다. IMaybe 가 있을 때마다 .Map() 를 호출하여 상호 작용할 수 있으며, None 로 판명되면 자동으로 실패합니다.

하지만 값을 풀어야 한다면 어떻게 될까요? 폴백 기능을 제공하여 안전하게 수행할 수 있습니다. Match라는 새 메서드를 구현해 보겠습니다(함수형 프로그래밍에서 패턴 일치와 같은 기능을 하기 때문).

아마.cs

public interface IMaybe<T>
{ 
    /// <summary> 
    /// applies `func` if and only if object exists 
    /// </summary> 
    /// <returns>a new Some of the result, or None if this is None</returns> 
    IMaybe<TNext> Map<TNext>(Func<T, TNext> func); 
    /// <summary> 
    /// applies `some` if value is present or `none` if no value.
    /// </summary> 
    /// <returns> an unwrapped value.</returns>
    TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none);
}

public static class Maybe
{ 
    public IMaybe<T> Factory(T member) 
      => member == null ? new None<T>() ? new Some<T>(member);
}

일부.cs

public class Some<T> : IMaybe<T>
{ 
    private T member; 
    internal Some(T member) 
    { 
        this.member = member; 
    } 

    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
      => Maybe.Factory(func(member)); 

    public TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none) 
      => some(member);
}

없음.cs

public class None<T> : IMaybe<T>
{ 
    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
        => new None<TNext>(); 
    public TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none) 
        => none();
}

이제 IMaybe 로 시작했기 때문에 안전하지 않은 값의 래핑을 해제할 때마다 기본값을 포함하는 것을 기억했는지 여부에 대해 걱정할 필요가 없습니다. C# 컴파일러는 이러한 종류의 작성을 허용하지 않습니다. 더 이상 버그.

그러나 이 코드는 모든 nullable 값을 안전하게 래핑하는 경우 작업하기가 다소 투박할 수 있습니다. 우리는 결국 읽고 이해하기 어렵게 되는 중첩IMaybe 모나드로 끝날 것입니다. 다음과 같이 FlatMap IMaybe 를 함께 사용하는 옵션을 포함하면 코드를 크게 단순화할 수 있습니다.

아마.cs

public interface IMaybe<T>
{ 
    /// <summary> 
    /// applies `func` and then flattens the result if the value
    /// exists. 
    /// </summary> 
    IMaybe<TNext> FlatMap<TNext>(Func<T, IMaybe<TNext>> func); 

    /// <summary> 
    /// applies `func` if and only if object exists 
    /// </summary> 
    /// <returns>a new Some of the result, or None if this is None</returns> 
    IMaybe<TNext> Map<TNext>(Func<T, TNext> func); 

    /// <summary> 
    /// applies `some` if value is present or `none` if no value.
    /// </summary> 
    /// <returns> an unwrapped value. 
    TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none);
}

public static class Maybe
{ 
    public IMaybe<T> Factory(T member) 
        => member == null ? new None<T>() ? new Some<T>(member);
}

일부.cs

public class Some<T> : IMaybe<T>
{ 
    private T member; 
    internal Some(T member) 
    { 
        this.member = member; 
    } 

    public IMaybe<TNext> FlatMap<TNext>(Func<T, IMaybe<TNext>> func) 
        => func(member); 

    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
        => Maybe.Factory(func(member)); 

    public TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none) 
        => some(member);
}

없음.cs

public class None<T> : IMaybe<T>
{ 
    public IMaybe<TNext> FlatMap<TNext>(Func<T, IMaybe<TNext>> func) 
        => new None<TNext>(); 

    public IMaybe<TNext> Map<TNext>(Func<T, TNext> func) 
        => new None<TNext>(); 

    public TNext Match<TNext>(Func<T, TNext> some, Func<TNext> none) 
        => none();
}

이제 여러 불확실한 작업을 연속으로 쉽게 처리할 수 있습니다. 이제 IMaybe 모나드에서 이러한 값을 래핑하는 습관을 들이면 확실히 NullReferenceException 피하게 될 것입니다. my next post 에서 C# 확장 메서드를 사용하여 목록 처리에서 모나드를 사용하는 방법을 보여줍니다.

좋은 웹페이지 즐겨찾기