atnos-eff의 Eff.Eff로 addLast 시뮬레이션

개시하다


atnos-eff는 Scara에서 Eff(More Extensible Effects)를 처리하는 데 사용되는 설명서입니다.Eff는 Oleg과 석정의 논문에서 제기되었지만atnos-eff에서 본고에서 기술하지 않은 기능을 제공했다Eff.addLast.addLast Eff 응용 해석기에 존재하고 마지막에 어떤 부작용을 수행한다.이 문서에서 먼저 addLast의 운동을 설명하고 이어서 문제점을 서술한다.이후 논문에서 기술한 Eff의 기능 시뮬레이션addLast을 활용해 기존addLast 문제를 해결하는 것을 목표로 한다.
또 이 기사에 등장하는 코드의 일부를 지티허브에 공개했다.
  • https://github.com/y-yu/atnos-eff-last-action
  • addLast 및 기존 문제


    addLast란?


    위에서 말한 바와 같이atnos-eff는Eff.addLast이고 다음과 같은 인터페이스이다.
    def addLast(l: =>Eff[R, Unit]): Eff[R, A] =
      addLast(Last.eff(l))
    
    private[eff] def addLast(l: Last[R]): Eff[R, A]
    
    Eff는 세 가지 데이터 유형PureImpure, 그리고 ImpureAp를 ADT[1]로 한다.다음 필드last가 각각 추가되어 있기 때문에 이렇게 사용할 수 있습니다addLast.하지만ImpureAp은 애플을 합성할 때 전용 처리(병행화 등)를 쓸 수 있도록 확장하기 위한 것이다.
    case class Pure[R, A](value: A, last: Last[R] = Last.none[R]) extends Eff[R, A] {
      def addLast(l: Last[R]): Eff[R, A] =
        Pure(value, last <* l)
    }
    
    case class Impure[R, X, A](union: Effect[R, X], continuation: Continuation[R, X, A], last: Last[R] = Last.none[R]) extends Eff[R, A] {
      def addLast(l: Last[R]): Eff[R, A] =
        Impure[R, X, A](union, continuation, last <* l)
    }
    
    case class ImpureAp[R, X, A](unions: Unions[R, X], continuation: Continuation[R, Vector[Any], A], last: Last[R] = Last.none[R]) extends Eff[R, A] {
      def addLast(l: Last[R]): Eff[R, A] =
        ImpureAp[R, X, A](unions, continuation, last <* l)
    }
    
    위에서 말한 바와 같이 모든 데이터last의 필드가 존재하고 addLastlast와 기존l과 입력<*을 결합한 형식으로 이루어진다.이productL는 다음과 같다.
    def <*(last: Last[R]): Last[R] =
      (value, last.value) match {
        case (None, None)       => this
        case (Some(r), None)    => this
        case (None, Some(l))    => last
        case (Some(r), Some(l)) => Last(Option(r.map2(l)((a, b) => b <* a)))
      }
    
    그리고 이런 방식으로 조합된<*은 마지막으로last의 함수Eff[NoFx, A] => A로 실행된다.

    문제점


    저자는 이용Eff.run이 큰 문제라고 생각한다.map2의 설명에서도 아트노스-eff에서는 애플리카티브와 몬드의 경우 효율화 등 목적에 따라 다른 동작을 할 수 있다고 살짝 언급했다.예를 들어ImpureAp의 행위를 감안하면 이해하기 쉽다.
  • Monad의 경우
  • Future가 되어 ma.flatMap(a => f(a))에 의존하는 함수ma를 집행하기 위해 집행f 이후 순차적으로 집행ma
  • 사과의 경우
  • f, ma.map2(mb)((a, b) => f(a, b))ma는 결과와 무관하므로 2개mb를 동시에 실행한 후
  • 이러한 차이로 인해 f부터 flatMap까지의 Applicative 실례는 자동으로 생성될 수 있지만 굳이 다른 실현을 실현함으로써 효율을 얻을 수 있다.예시map2에서 보듯이 Scara에서는 이러한 Applicative에서 행동을 바꾸기 때문에 (실제로는 Monad 실례와 일치하지 않기 때문에) atnos-eff와 Applicative는 더욱 효율적인 다른 처리를 써서 지원할 수 있을 것이다.
    이 토론을 바탕으로 합성Future에 주목할 것이다.현재 합성된 코드는 addLast인데 그 위에 모형을 더하면 다음과 같다.
    Last(Option(
      (r: Eval[Eff[R, Unit]]).map2(l: Eval[Eff[R, Unit]])
        ((a: Eff[R, Unit], b: Eff[R, Unit]) => b <* a)
    ))
    
    즉 두 개Last(Option(r.map2(l)((a, b) => b <* a)))의 값Effa을 통과b(=<*로 합성한다.결과적으로 b.map2(a){(b, _) => b} 는 Applicative 값으로 실행됩니다.이것은 해석기addLast의 추가 순서와 실제 실행 순서가 완전히 일치하지 않기 때문이라고 할 수 있다.따라서 예를 들어 주 코드가 addLast 등에서 비동기적이어도 종결 처리를 순서대로 진행할 수 없다.

    atnos-eff-last-action


    그래서 이번에는 Eff의 개조로 이런 행동에 대응하지 않았다.원본 코드는 다음 창고에 존재합니다.
  • https://github.com/y-yu/atnos-eff-last-action
  • 먼저 사례류 등을 정의한다.
    sealed trait LastAction[A]
     
    case class SideEffectLastAction(
      value: Eval[Unit]
    ) extends LastAction[Unit]
     
    type _last[R] = LastAction |= R
    
    그리고 해석기는 다음과 같다.
    def runLast[U](implicit
      m: Member.Aux[LastAction, R, U]
    ): Eff[U, A] =
      Interpret
        .runInterpreter(eff)(new Interpreter[LastAction, U, A, Eval[A]] {
          override def onPure(a: A): Eff[U, Eval[A]] = {
            Eff.pure(Eval.now(a))
          }
     
          override def onEffect[X](
            x: LastAction[X],
            continuation: Continuation[U, X, Eval[A]]
          ): Eff[U, Eval[A]] = {
            x match {
              case SideEffectLastAction(value) =>
                continuation(())
                  .map { eval =>
                    value >> eval
                  }
            }
          }
     
          override def onLastEffect[X](
            x: LastAction[X],
            continuation: Continuation[U, X, Unit]
          ): Eff[U, Unit] =
            x match {
              case SideEffectLastAction(value) =>
                continuation(()).map(_ => value.value)
            }
     
          override def onApplicativeEffect[X, T[_] : Traverse](
            xs: T[LastAction[X]],
            continuation: Continuation[U, T[X], Eval[A]]
          ): Eff[U, Eval[A]] = {
     
            continuation
              .apply(xs.map { case SideEffectLastAction(_) => () })
              .map { eval =>
                xs.foldLeft(Eval.Unit) {
                  case (acc, SideEffectLastAction(value)) =>
                    value >> acc
                } >> eval
              }
          }
        })
        .map(_.value)
    
    간단하게 설명하면 다음과 같다.

  • 패턴 일치Task를 통해 GADTsSideEffectLastAction의 유형LastAction[X]X으로 고정Unit
  • 를 통해 continuation 중 기동이 계속될 수 있음(): Unit
  • (=*>))>>가 아닌ma.flatMap(a => f(a).map(a)) 결합 사용
  • 이런 방식을 전용 효과로 삼아 Monad나 ApplicativeEff.addLast의 차이에 신경 쓰지 않고 Go 언어defer처럼 상반된 순서로 실행하는 행동을 간단하게 할 수 있다.

    총결산


    대체로 atnos-eff에 홍보를 보냈습니다.
  • https://github.com/atnos-org/eff/pull/292
  • 다만 이것은 합병됩니까?잘 모르겠어요.이것은 원래 논문에 없는 행위인데, 작가에게 어느 것이 비교적 좋아요?그런 판단을 내리지 않았기 때문이다.
    아무래도 addLast 목표대로 움직이면 상황에 따라 스스로 건조기를 만들어도 해결되지 않아 포크 등이 필요해 번거롭다.다른 한편, 이렇게 해석기를 사용한 수법이라면 스스로 손만 대면 많은 행동을 바꿀 수 있기 때문에 더 좋다고 생각한다.
    각주ImpureAp 원래의 논문에 나타나지 않았다.↩︎

    좋은 웹페이지 즐겨찾기