Skip to content

Commit c0e65c4

Browse files
Match query type variables against concrete types (#488)
A type search for `a -> HTMLElement` previously returned no concrete results where `_ -> HTMLElement` matched functions like `HTMLAnchorElement -> HTMLElement`, because compareTypes had no case for a query-side type variable against a concrete type (#395). Query variables now match any concrete type, as wildcards do, except that each instantiation charges a penalty of 1 - less than a single unit of structural mismatch (10) - so results that unify with the query directly still rank first. Each instantiation is also recorded against the variable's name and fed through typeVarPenalty, so repeated query variables must be instantiated consistently: for the query `a -> a`, `Int -> Int` ranks above `Int -> String`. Existing comparisons are unaffected: no previously-matching clause changed, so current rankings only gain new, lower-ranked results. Also fixes the compareTypes doc examples, which had a typo (parseType s2 twice) and a stale expected score predating the 10x score scaling. Based on #396 by @klntsky.
1 parent d1c6bfd commit c0e65c4

3 files changed

Lines changed: 57 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ the most up-to-date version of this file.
55

66
## Unreleased
77

8+
- Type search queries containing type variables now also match more concrete
9+
types: `a -> HTMLElement` finds `HTMLAnchorElement -> HTMLElement` the same
10+
way `_ -> HTMLElement` does (#395). Unlike a wildcard, instantiating a query
11+
variable charges a small penalty, so results that unify with the query
12+
directly rank first, and repeated variables must be instantiated
13+
consistently (`a -> a` ranks `Int -> Int` above `Int -> String`). Based on
14+
#396 by @klntsky. (@thomashoneyman)
815
- The package and module badges on search results are now links to the
916
package page and module docs page (#424, @joprice). Builtin modules such
1017
as Prim have no package page, so their package badge remains plain text.

src/SearchIndex.hs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -403,26 +403,37 @@ tryStripPrefix pre s = fromMaybe s (T.stripPrefix pre s)
403403
-- The first argument is the query, and the second is the candidate result.
404404
-- This function is not symmetric; for example:
405405
--
406-
-- let compare s1 s2 = compareTypes <$> parseType s2 <*> parseType s2
406+
-- let compare s1 s2 = compareTypes <$> parseType s1 <*> parseType s2
407407
--
408408
-- >>> compare "a" "Int"
409-
-- Just Nothing
410-
-- >>> compare "Int" "a"
411409
-- Just (Just 1)
410+
-- >>> compare "Int" "a"
411+
-- Just (Just 10)
412412
--
413-
-- (The idea here being it's ok to show a more general version of the query,
414-
-- but usually not helpful to show a more concrete version of it.)
413+
-- (The idea here being that a result which is more concrete than the query
414+
-- is a slightly worse match than one which unifies with the query directly,
415+
-- but a much better match than one which is more general than the query.)
415416
--
416417
compareTypes :: D.Type' -> D.Type' -> Maybe Int
417418
compareTypes type1 type2 =
418419
map calculate . runWriterT $ go type1 type2
419420
where
420-
calculate :: (Int, [(Text, Text)]) -> Int
421-
calculate (score, vars) = (10 * score) + typeVarPenalty vars
422-
423-
go :: D.Type' -> D.Type' -> WriterT [(Text, Text)] Maybe Int
424-
go (P.TypeVar _ v1) (P.TypeVar _ v2) = tell [(v1, v2)] *> pure 0
421+
-- Each instantiation of a query variable with a concrete type costs 1,
422+
-- deliberately less than a single unit of structural mismatch (10), so
423+
-- that results which unify with the query rank above instantiations of it.
424+
calculate :: (Int, ([(Text, Text)], [(Text, Text)])) -> Int
425+
calculate (score, (vars, insts)) =
426+
(10 * score) + typeVarPenalty (vars ++ insts) + length insts
427+
428+
go :: D.Type' -> D.Type' -> WriterT ([(Text, Text)], [(Text, Text)]) Maybe Int
429+
go (P.TypeVar _ v1) (P.TypeVar _ v2) = tell ([(v1, v2)], []) *> pure 0
425430
go t (P.TypeVar _ _) = pure (1 + typeComplexity t)
431+
-- A type variable in the query matches any concrete type, like a wildcard,
432+
-- except that it is charged an instantiation penalty and the pairing is
433+
-- recorded, so that repeated query variables are penalised for matching
434+
-- inconsistently (the rendered type acts as a result-side variable in
435+
-- 'typeVarPenalty').
436+
go (P.TypeVar _ v) t = tell ([], [(v, typeToText t)]) *> pure (typeComplexity t)
426437
go (P.TypeLevelString _ s1) (P.TypeLevelString _ s2) | s1 == s2 = pure 0
427438
go (P.TypeWildcard _ _) t = pure (typeComplexity t)
428439
go (P.TypeConstructor _ q1) (P.TypeConstructor _ q2) | compareQual q1 q2 = pure 0
@@ -448,7 +459,7 @@ compareTypes type1 type2 =
448459
go t1 (P.ParensInType _ t2) = go t1 t2
449460
go _ _ = lift Nothing
450461

451-
goRows :: D.Type' -> D.Type' -> WriterT [(Text, Text)] Maybe Int
462+
goRows :: D.Type' -> D.Type' -> WriterT ([(Text, Text)], [(Text, Text)]) Maybe Int
452463
goRows r1 r2 = sum <$>
453464
sequence [ go t1 t2
454465
| P.RowListItem _ name t1 <- fst (P.rowToList r1)
@@ -459,7 +470,10 @@ compareTypes type1 type2 =
459470
-- Calculate a penalty based on the extent to which the type variables match.
460471
-- Where differences occur, those which make the result more general than the
461472
-- query are not penalised as harshly as those which make the result less
462-
-- general than the query.
473+
-- general than the query. The list may pair a query variable with a rendered
474+
-- concrete type as well as with a result variable; an inconsistently
475+
-- instantiated query variable is penalised in the same way as one matching
476+
-- several distinct result variables.
463477
typeVarPenalty :: [(Text, Text)] -> Int
464478
typeVarPenalty list =
465479
penalty list + (3 * penalty (map swap list))

test/SearchSpec.hs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,27 @@ spec = do
121121
x <- shouldMatch query cand0
122122
y <- shouldMatch query cand1
123123
x `shouldBeLessThan` y
124+
125+
describe "with query variables against concrete types (#395)" $ do
126+
it "matches concrete instantiations of the query" $ do
127+
void $ shouldMatch (p "a -> Int") (p "String -> Int")
128+
129+
it "treats variables like wildcards, with a small penalty" $ do
130+
x <- shouldMatch (p "_ -> Int") (p "String -> Int")
131+
y <- shouldMatch (p "a -> Int") (p "String -> Int")
132+
x `shouldBeLessThan` y
133+
134+
it "prefers unifying results to concrete instantiations" $ do
135+
x <- shouldMatch (p "a -> a") (p "x -> x")
136+
y <- shouldMatch (p "a -> a") (p "Int -> Int")
137+
x `shouldBeLessThan` y
138+
139+
it "prefers consistent instantiations to inconsistent ones" $ do
140+
x <- shouldMatch (p "a -> a") (p "Int -> Int")
141+
y <- shouldMatch (p "a -> a") (p "Int -> String")
142+
x `shouldBeLessThan` y
143+
144+
it "prefers instantiation to generalization" $ do
145+
x <- shouldMatch (p "a -> Int") (p "String -> Int")
146+
y <- shouldMatch (p "a -> Int") (p "x -> y")
147+
x `shouldBeLessThan` y

0 commit comments

Comments
 (0)