package scalaz

sealed trait Kleisli[M[_], A, B] {
  def apply(a: A): M[B]

  import Scalaz._

  def >=>[C](k: Kleisli[M, B, C])(implicit b: Bind[M]): Kleisli[M, A, C] = ((a: A) => b.bind(this(a), k(_: B)))

  def >=>[C](k: B => M[C])(implicit b: Bind[M]): Kleisli[M, A, C] = >=>((k))

  def <=<[C](k: Kleisli[M, C, A])(implicit b: Bind[M]): Kleisli[M, C, B] = k >=> this

  def <=<[C](k: C => M[A])(implicit b: Bind[M]): Kleisli[M, C, B] = (k) >=> this

  def compose[N[_]](f: M[B] => N[B]): Kleisli[N, A, B] = ((a: A) => f(this(a)))

  def traverse[F[_], AA <: A](f: F[AA])(implicit a: Applicative[M], t: Traverse[F]): M[F[B]] =
    f  (Kleisli.this(_)) 

  def =<<[AA <: A](a: M[AA])(implicit m: Bind[M]): M[B] = m.bind(a, apply _)  
  
  def map[C](f: B => C)(implicit m: Functor[M]): Kleisli[M, A, C] =
    kleisli(a => m.fmap(apply(a), f))

  def flatMap[C](f: B => M[C])(implicit m: Monad[M]): Kleisli[M, A, C] =
    kleisli(a => m.bind(apply(a), f))
}

trait Kleislis {
  def kleisli[M[_], A, B](f: A => M[B]): Kleisli[M, A, B] = new Kleisli[M, A, B] {
    def apply(a: A) = f(a)
  }

  def [M[_], A, B](f: A => M[B]): Kleisli[M, A, B] = kleisli(f)

  implicit def kleisliFn[M[_],A,B](k: Kleisli[M,A,B]): A => M[B] = (a: A) => k(a)

  /** Pure Kleisli arrow */
  def ask[M[_]: Monad, A]: Kleisli[M, A, A] = kleisli(a => implicitly[Monad[M]].pure(a))

  def kleisliPure[M[_], R](implicit m: Pure[M])
    : Pure[({type λ[x]=Kleisli[M, R, x]})] =
  new Pure[({type λ[x]=Kleisli[M, R, x]})] {
      def pure[A](a: => A) = kleisli((r: R) => m.pure(a))
    }

/*
  implicit def kleisliBind[M[_]:Bind, R]
    : Bind[({type λ[x]=Kleisli[M, R, x]})#λ] =
  new Bind[({type λ[x]=Kleisli[M, R, x]})#λ] {
      def bind[A,B](m: Kleisli[M, R, A], k: A => Kleisli[M, R, B]) =
        kleisli((r: R) => m.apply(r) >>= ((a: A) => k(a).apply(r)))
    }
*/

  implicit def kleisliBind[M[_], R](implicit b: Bind[M])
    : Bind[({type λ[x]=Kleisli[M, R, x]})] =
  new Bind[({type λ[x]=Kleisli[M, R, x]})] {
      def bind[A,B](m: Kleisli[M, R, A], k: A => Kleisli[M, R, B]) =
        kleisli((r: R) => b.bind[A,B](m.apply(r),((a: A) => k(a).apply(r))))
    }
}