Type-directed search with dependent types

Ben Sherman

August 12, 2014

Types have the potential to be, all at once, specifications, documentation, and proofs of correctness. Because Haskell's got a powerful type system, Hoogle, a tool for finding terms by approximate type signature, proves rather valuable. But in a dependently typed language, types reach their pinnacle. The combined variety and precision of types in such languages means that type-directed search is a really enticing proposition.

So I decided to create such a type-directed search! I wrote the :search function for Idris, a dependently typed programming language inspired by Haskell. I took an approach guided by type isomorphism (which is somewhat different from Hoogle): when :search matches two types, one can be constructed from the other.

This is a talk which I gave at the Haskell DC meetup in Washington, D.C on August 12, 2014.

In the first part of the talk, I discuss how programming languages vary in terms of what can be ascertained statically about their programs, particularly in regards to type systems. When type systems are powerful, as they are in Idris, I prefer type-driven development to test-driven development. I demonstrate that the property map f . map g = map (f . g) is false for ML but prove its veracity for Haskell by hand.

1 import Control.Monad ((<=<)) 2 import System.IO (withFile, IOMode (ReadMode), hGetChar, Handle) 3 4 5 6 7 main :: IO () 8 main = do 9 putStrLn "read1:" 10 f read1 11 putStrLn "read2:" 12 f read2 13 where 14 f reader = withFile "haskell.txt" ReadMode $ \ h -> do 15 xs <- reader h [ 1 .. 30 ] 16 putStrLn xs 17 18 19 20 read1 :: Handle -> [a] -> IO String 21 read1 h = mapM (get <=< get) where 22 get = const (hGetChar h) 23 24 25 read2 :: Handle -> [a] -> IO String 26 read2 h = mapM get <=< mapM get 27 where get = const (hGetChar h) 1 map :: (a -> b) -> [a] -> [b] 2 map f [] = [] 3 map f (x : xs) = f x : map f xs 4 5 ( . ) :: (b -> c) -> (a -> b) -> (a -> c) 6 ( . ) f g x = f (g x) 7 8 9 =============================== 10 11 (map f . map g) [] 12 ( . ) (map f) (map g) [] 13 (map f) (map g []) 14 map f [] 15 [] 16 17 map (f . g) [] 18 [] 19 20 =============================== 21 22 (map f . map g) (x : xs) 23 ( . ) (map f) (map g) (x : xs) 24 (map f) (map g (x : xs)) 25 map f (g x : map g xs) 26 f (g x) : map f (map g xs) 27 28 29 map (f . g) (x : xs) 30 (f . g) x : map (f . g) xs 31 f (g x) : map (f . g) xs 32

In the second part of the talk, I construct a term in Idris which bears witness to the fact that map f . map g = map (f . g) is true in Idris. I demonstrate the :search feature in Idris and explain how it works. I also discuss some strategies I employed and issues I faced when developing :search, and ways in which :search can be improved in the future.

1 eqList : { a : Type } -> { x , y : a } -> { xs , ys : List a } 2 -> x = y -> xs = ys -> x :: xs = y :: ys 3 eqList { x } { y } { xs } { ys } p q = replace { P =\ ts => x :: xs = y :: ts } q 4 ( replace { P =\ t => x :: xs = t :: xs } p ( refl { x :: xs })) 5 6 7 8 9 mapFusion : { a , b , c : Type } 10 -> ( f : a -> b ) -> ( g : b -> c ) -> ( xs : List a ) 11 -> map ( g . f ) xs = map g ( map f xs ) 12 mapFusion f g [] = refl 13 mapFusion f g ( x :: xs ) = eqList refl ( mapFusion f g xs ) 14 Idris> :s Type -> Type = Prelude.Algebra.AbelianGroup : Type -> Type Sets equipped with a single binary operation that is associative and commutative, along with a neutral element for that binary operation and inverses for all elements. Must satisfy the following laws: = Prelude.Algebra.BoundedJoinSemilattice : Type -> Type Sets equipped with a binary operation that is commutative, associative and idempotent and supplied with a neutral element. Must satisfy the following laws: = Prelude.Algebra.BoundedLattice : Type -> Type Sets equipped with two binary operations that are both commutative, associative and idempotent and supplied with neutral elements, along with absorbtion laws for relating the two binary operations. Must satisfy the following: = Prelude.Algebra.BoundedMeetSemilattice : Type -> Type Sets equipped with a binary operation that is commutative, associative and idempotent and supplied with a neutral element. Must satisfy the following laws: = Prelude.Basics.Dec : Type -> Type Decidability. A decidable property either holds or is a contradiction. = Decidable.Equality.DecEq : Type -> Type Decision procedures for propositional equality = Prelude.Enum : Type -> Type = Prelude.Classes.Eq : Type -> Type The Eq class defines inequality and equality. = Foreign : Type -> Type = Prelude.Algebra.Group : Type -> Type Sets equipped with a single binary operation that is associative, along with a neutral element for that binary operation and inverses for all elements. Must satisfy the following laws: = IO : Type -> Type = Inf : Type -> Type Recursive parameters to codata. Inserted automatically by the elaborator on a "codata" definition but is necessary by hand if mixing inductive and coinductive parameters. = Prelude.Classes.Integral : Type -> Type = Prelude.Algebra.JoinSemilattice : Type -> Type Sets equipped with a binary operation that is commutative, associative and idempotent. Must satisfy the following laws: = Prelude.Algebra.Lattice : Type -> Type Sets equipped with two binary operations that are both commutative, associative and idempotent, along with absorbtion laws for relating the two binary operations. Must satisfy the following: = Lazy : Type -> Type Lazily evaluated values. This has special evaluation semantics. = Prelude.List.List : Type -> Type Linked lists = Prelude.Classes.MaxBound : Type -> Type = Prelude.Maybe.Maybe : Type -> Type = Prelude.Algebra.MeetSemilattice : Type -> Type Sets equipped with a binary operation that is commutative, associative and idempotent. Must satisfy the following laws: = Prelude.Classes.MinBound : Type -> Type = Prelude.Algebra.Monoid : Type -> Type Sets equipped with a single binary operation that is associative, along with a neutral element for that binary operation. Must satisfy the following laws: = Prelude.Basics.Not : Type -> Type = Prelude.Classes.Num : Type -> Type The Num class defines basic numerical arithmetic. = Prelude.Classes.Ord : Type -> Type The Ord class defines comparison operations on ordered data types. = PrimIO : Type -> Type Idris's primitive IO, for building abstractions on top of = Prelude.Algebra.Ring : Type -> Type Sets equipped with two binary operations, one associative and commutative supplied with a neutral element, and the other associative, with distributivity laws relating the two operations. Must satisfy the following laws: = Prelude.Algebra.RingWithUnity : Type -> Type Sets equipped with two binary operations, one associative and commutative supplied with a neutral element, and the other associative supplied with a neutral element, with distributivity laws relating the two operations. Must satisfy the following laws: = Prelude.Algebra.Semigroup : Type -> Type Sets equipped with a single binary operation that is associative. Must satisfy the following laws: = Prelude.Show : Type -> Type = Prelude.Stream.Stream : Type -> Type An infinite stream = Prelude.Uninhabited.Uninhabited : Type -> Type A canonical proof that some type is empty = Prelude.Algebra.VerifiedAbelianGroup : Type -> Type = Prelude.Algebra.VerifiedBoundedJoinSemilattice : Type -> Type = Prelude.Algebra.VerifiedBoundedLattice : Type -> Type = Prelude.Algebra.VerifiedBoundedMeetSemilattice : Type -> Type = Prelude.Algebra.VerifiedGroup : Type -> Type = Prelude.Algebra.VerifiedJoinSemilattice : Type -> Type = Prelude.Algebra.VerifiedLattice : Type -> Type = Prelude.Algebra.VerifiedMeetSemilattice : Type -> Type = Prelude.Algebra.VerifiedMonoid : Type -> Type = Prelude.Algebra.VerifiedRing : Type -> Type = Prelude.Algebra.VerifiedRingWithUnity : Type -> Type = Prelude.Algebra.VerifiedSemigroup : Type -> Type = WorldRes : Type -> Type = (__Elim) : Type -> Type Type class for eliminators < assert_total : a -> a Assert to the totality checker than the given expression will always terminate. < Prelude.Basics.id : a -> a Identity function. < Prelude.Basics.the : (a : Type) -> a -> a Manually assign a type to an expression. < trace_malloc : a -> a Idris> :s Ord a => List a -> List a = Prelude.List.sort : Ord a => List a -> List a < Prelude.List.reverse : List a -> List a Return the elements of a list in reverse order. < Prelude.List.nub : Eq a => List a -> List a < assert_total : a -> a Assert to the totality checker than the given expression will always terminate. < Prelude.Basics.id : a -> a Identity function. < Prelude.Basics.the : (a : Type) -> a -> a Manually assign a type to an expression. < trace_malloc : a -> a < Prelude.List.toList : Foldable t => t a -> List a Convert any Foldable structure to a list. Idris> :s plus x y = plus y x = Prelude.Nat.plusCommutative : (left : Nat) -> (right : Nat) -> left + right = right + left > Prelude.Nat.multOneLeftNeutral : (right : Nat) -> fromInteger 1 * right = right > Prelude.Nat.plusZeroRightNeutral : (left : Nat) -> left + fromInteger 0 = left Idris> :t plus Prelude.Nat.plus : Nat -> Nat -> Nat Idris> :t the Nat 3 the Nat (fromInteger 3) : Nat Idris> the Nat 3 + 5 8 : Nat Idris> :t Vect Prelude.Vect.Vect : Nat -> Type -> Type Idris> :t (++) Prelude.List.(++) : List a -> List a -> List a Prelude.Strings.(++) : String -> String -> String Prelude.Vect.(++) : Vect m a -> Vect n a -> Vect (m + n) a Idris> :t Either Prelude.Either.Either : Type -> Type -> Type Idris> :t head Prelude.List.head : (l : List a) -> (isCons l = True) -> a Prelude.Stream.head : Stream a -> a Prelude.Vect.head : Vect (S n) a -> a Idris> :s Vect (S n) a -> a = Prelude.Vect.head : Vect (S n) a -> a Only the first element of the vector = Prelude.Vect.last : Vect (S n) a -> a The last element of the vector Idris> :s Functor f => f a -> f b -> f (a, b) > Prelude.Vect.zip : Vect n a -> Vect n b -> Vect n (a, b) Combine two equal-length vectors pairwise > Prelude.Stream.zip : Stream a -> Stream b -> Stream (a, b) Create a stream of pairs from two streams Idris> :s (m, n : Nat) -> m + n = n + m = Prelude.Nat.plusCommutative : (left : Nat) -> (right : Nat) -> left + right = right + left > Prelude.Nat.multOneLeftNeutral : (right : Nat) -> fromInteger 1 * right = right > Prelude.Nat.plusZeroRightNeutral : (left : Nat) -> left + fromInteger 0 = left Idris> :s Ord a => a -> a -> Bool = Prelude.Classes.(<) : Ord a => a -> a -> Bool = Prelude.Classes.(<=) : Ord a => a -> a -> Bool = Prelude.Classes.(>) : Ord a => a -> a -> Bool = Prelude.Classes.(>=) : Ord a => a -> a -> Bool < Prelude.Classes.(/=) : Eq a => a -> a -> Bool < Prelude.Classes.(==) : Eq a => a -> a -> Bool > Prelude.Nat.gt : Nat -> Nat -> Bool Boolean test than one Nat is strictly greater than another > Prelude.Nat.gte : Nat -> Nat -> Bool Boolean test than one Nat is greater than or equal to another > Prelude.Strings.isInfixOf : String -> String -> Bool > Prelude.Strings.isPrefixOf : String -> String -> Bool > Prelude.Strings.isSuffixOf : String -> String -> Bool > Prelude.Nat.lt : Nat -> Nat -> Bool Boolean test than one Nat is strictly less than another > Prelude.Nat.lte : Nat -> Nat -> Bool Boolean test than one Nat is less than or equal to another Idris> :s fact 5 = the Nat 100 Idris> :s fact 5 = the Nat 120 < Prelude.Nat.maximumZeroNRight : (right : Nat) -> maximum 0 right = right < Prelude.Nat.plusOneSucc : (right : Nat) -> fromInteger 1 + right = S right < Prelude.Nat.plusZeroLeftNeutral : (right : Nat) -> fromInteger 0 + right = right < Prelude.Nat.predSucc : (n : Nat) -> pred (S n) = n < refl : x = x A proof that x in fact equals x. To construct this, you must have already shown that both sides are in fact equal. Idris> :s (Ord a, Ord b) => (a, b) -> (a, b) -> Bool < Prelude.Classes.(<) : Ord a => a -> a -> Bool < Prelude.Classes.(<=) : Ord a => a -> a -> Bool < Prelude.Classes.(>) : Ord a => a -> a -> Bool < Prelude.Classes.(>=) : Ord a => a -> a -> Bool < Prelude.Classes.(/=) : Eq a => a -> a -> Bool < Prelude.Classes.(==) : Eq a => a -> a -> Bool

View slides in full (PDF).