Functional Programming/Category Theory

Contravariant Functor, Profunctor

yogingang 2022. 11. 14. 00:00
반응형

Contravariant Functor

화살표의 방향이 반대가 되는 Functor

a - b 가 아니고 b - a 이다.

 

 

입력과 출력이 반대가 되는 Functor이다. 

반공변 Functor 라고 할 수 있다. 

 

Hashkell 에서는 다음과 같이 정의 한다.

contramap :: Contravariant f => (b -> a) -> f a -> f b

C# 은 아래와 같은 형태 이다.

public Contravariant<T1> ContraMap<T1>(Func<T1, T> selector)

 

C# 에서 covariant functor (일반 Functor)

// Covariance.
IEnumerable<string> strings = new List<string>();
// An object that is instantiated with a more derived type argument
// is assigned to an object instantiated with a less derived type argument.
// Assignment compatibility is preserved.
IEnumerable<object> objects = strings;

IEnumerable<T> 를 통해 대표적인 Covariant functor 를 표현했다.

// Contravariance.
// Assume that the following method is in the class:
// static void SetObject(object o) { }
Action<object> actObject = SetObject;
// An object that is instantiated with a less derived type argument
// is assigned to an object instantiated with a more derived type argument.
// Assignment compatibility is reversed.
Action<string> actString = actObject;

Action<T> 를 통해 대표적인 Contravariant functor 를 표현했다.

 

즉 Contravariant functor 는 Adapter 의 역할을 한다고 생각하면 쉽다. 

즉 입력으로 int 값을 받아야 하는데 int 가 아닌 string 을 받는 function 이 있다면

일단 string 을 int 로 변형하는 Conversion(Selector) 를 구현하고

그 Selector 를 먼저 실행시켜서 string 을 int 로 변형하고

실제 적용하려는 Functor 를 그 이후에 실행 하면 된다. 

이런식으로 사용하기 위해 Contravariant functor 가 존재 한다.

 

아래는 이를 간단히 예로든 코드 이다.

public interface IReader<R, A>
{
    A Run(R environment);
}
public static partial class IReaderExtension
{
    public static IReader<R1, A> ContraMap<R, R1, A>(this IReader<R, A> reader,
        Func<R1, R> selector)
    {
        return new FuncReader<R1, A>(r => reader.Run(selector(r)));
    }

    private sealed class FuncReader<R, A> : IReader<R, A>
    {
        private readonly Func<R, A> func;

        public FuncReader(Func<R, A> func)
        {
            this.func = func;
        }

        public A Run(R environment)
        {
            return func(environment);
        }
    }
}

public sealed class MinutesReader : IReader<int, TimeSpan>
{
    public TimeSpan Run(int environment)
    {
        return TimeSpan.FromMinutes(environment);
    }
}

public sealed class BooleanReader : IReader<bool, int>
{
    public int Run(bool environment)
    {
        return environment ? 1:2;
    }
}

// projected, tProjected  는 string -> int 로
// string --> bool 로 변경하는 역할을 하는 Conversion 함수를 
// ContraMap 에 적용시킨다. 
// 이럴경우 ContraMap 은 adapter 역할을 하게 된다. 

IReader<int, TimeSpan> r = new MinutesReader();
IReader<string, TimeSpan> projected = r.ContraMap((string s) => int.Parse(s));
WriteLine(new TimeSpan(0, 21, 0) == projected.Run("21"));

IReader<bool, int> t = new BooleanReader();
IReader<string,int> tProjected = t.ContraMap((string s) => bool.Parse(s));
WriteLine(2 == tProjected.Run("False"));

문자열 "21" 을 ContraMap 에 정수형 으로 변경하는 (int.Parse) 함수를 적용하고 

해당 Functor(Contravariant) 를 이용하여 run 하고 있다.

(adaptor 역할을 하고 있다. )

 

즉 Selector 가 먼저 Reader 가 나중에 실행된다. 

 

원래 Map 같은 경우는 아래와 같다. 

public static IReader<R, B> Map<A, B, R>(this IReader<R, A> reader, 
    Func<A, B> selector)
{
    return new FuncReader<R, B>(r => selector(reader.Run(r)));
}

Reader 가 먼저 Selector 가 나중에 실행된다. 

Reader --> SelectorContraMap 에서는 Selector --> Reader 로 반전 되어 있는 형태이다.

 

 

 

ProFunctor 

BiFunctor 에서 argument 중 하나에 Contravariant 을 적용시킨 Functor 이다.

일반 적으로 첫번재 argument 에 대해 Contravariant 를

두번째 변수에는 Covariant 를 적용시킨다.

Profunctor<A,B>  에서 A 는 Contravariant, B 는 Covariant 이다.

그리고 이러한 mapping 을 지원하기 위한 함수 를 DiMap 이라고 한다. 

위 IReader 에 대한 DiMap 을 구현해 보자

public static partial class IReaderExtension
{
    public static IReader<R1, A1> DiMap<R, R1, A, A1>(
    this IReader<R, A> reader,
    Func<R1, R> contraSelector,
    Func<A, A1> coSelector)
    {
        return reader.ContraMap(contraSelector).Map(coSelector);
    }
    ...
    ...
}

IReader 를 이용하여 TotalSecondsReader 를  구현해보자

public sealed class TotalSecondsReader : IReader<DateTimeOffset, double>
{
    public double Run(DateTimeOffset environment)
    {
        WriteLine(environment.ToUnixTimeSeconds());
        return environment.ToUnixTimeSeconds();
    }
}

마지막으로 사용해 보자

IReader<DateTimeOffset, double> reader = new TotalSecondsReader();
IReader<DateTime, bool> projection =
    reader.DiMap((DateTime dt) => DateTime.SpecifyKind(dt, DateTimeKind.Local), d => d % 2 == 0);
WriteLine(projection.Run(DateTime.Now));

현재시간을 받아서 그것을 DateTimeOffset 형태로 바꾸고 해당 하는 값을 totalseconds 로 변경한다. 

그게 첫번째 argument 인 Contravariant 가 하는 일이고 이것은 ContraMap 에 의해 처리 된다.. 

두번째 argument 는 첫번째 argument 의 결과 값을 2로 나누어 남은 나머지가 0 인지 비교한다. 

즉 event 값인지 체크하고 true 또는 false 를 return 하게 된다. 

 

 

 

관련영상

https://youtu.be/-MPcu4S3jmQ

 

 

반응형