From a596ece77fd3706529d959fe3d7559d5fed87407 Mon Sep 17 00:00:00 2001 From: Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> Date: Mon, 13 Feb 2023 21:07:20 +0100 Subject: [PATCH 1/7] Add introduction --- .../pangram/.approaches/introduction.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 exercises/practice/pangram/.approaches/introduction.md diff --git a/exercises/practice/pangram/.approaches/introduction.md b/exercises/practice/pangram/.approaches/introduction.md new file mode 100644 index 000000000..f1fabd1cc --- /dev/null +++ b/exercises/practice/pangram/.approaches/introduction.md @@ -0,0 +1,94 @@ +# Introduction + +The key to solving this exercise is checking that all letters of the alphabet are present in the input. +This can be done in a great variety of ways. + +Solutions to this problem tend to fall in one of four categories: + +- Some solutions iterate over the alphabet first, others over the input. +- Some solutions can deal with infinite input, others cannot. + +Why care about whether you can deal with infinite input? +Well, if you cannot, then you are (almost?) certainly doing unnecessary work _somewhere_. +(If your problem is solvable with finite effort, that is.) +If you can deal with infinite input just fine, this is a hint that your strategy is efficient. + +Also, reasoning about infinite data can be great fun 😁 + +All the approaches highlighted above have variants that use the `Set` data structure. +You can find the code in the linked approach documents. + + +## Approach: express the definition of _pangram_ + +```haskell +isPangram :: String -> Bool +isPangram text = all (`elem` map toLower text) ['a' .. 'z'] +``` + +This solution is simply the definition of _pangram_ put into ~~words~~ code. +A sentence is a pangram when `all` letters of the alphabet are present. + +This solution iterates over the alphabet first and tolerates infinite input. +However, it is inefficient for some inputs. +This can be remedied using `Set`, at the cost of losing tolerance for infinite input. + +[Read more about this approach][all]. + + +## Approach: check that the alphabet is a substructure of the input + +```haskell +isPangram :: String -> Bool +isPangram = (['a' .. 'z'] `isSubsequenceOf`) . sort . map toLower +``` + +This approach uses `isSubsequenceOf` or `isSubsetOf` to check that the alphabet forms a subsequence/subset of the input. + +Due to `sort` or `Set.fromList`, this approach does not tolerate infinite input. +It is efficient though: after sorting/accumulating, both the alphabet and the input are walked only once. + +[Read more about this approach][substructure]. + + +## Approach: cross off all characters at once + +```haskell +isPangram :: String -> Bool +isPangram text = null (['a' .. 'z'] \\ map toLower text) +``` + +This solution uses `(\\)` to remove all letters in the input from the alphabet. +Finally, it checks that all letters were removed. + +Perhaps surprisingly, this solution does _not_ tolerate infinite input. + +[Read more about this approach][cross-off-all]. + + +## Approach: cross off characters as you go and stop when none remain + +```haskell +isPangram :: String -> Bool +isPangram = any null . scanl (flip delete) ['a' .. 'z'] . map toLower +``` + +This approach is the most complicated one on this page. +It is also the most efficient. +In theory at least. + +[Read more about this approach][cross-off-one-by-one]. + + +[all]: + https://exercism.org/tracks/haskell/exercises/pangram/approaches/all + "Approach: use all to express what a pangram is" +[cross-off-all]: + https://exercism.org/tracks/haskell/exercises/pangram/approaches/cross-off-all + "Approach: cross off all characters at once" +[cross-off-one-by-one]: + https://exercism.org/tracks/haskell/exercises/pangram/approaches/cross-off-one-by-one + "Approach: cross off characters as you go and stop when none remain" +[substructure]: + https://exercism.org/tracks/haskell/exercises/pangram/approaches/substructure + "Approach: check that the alphabet is a substructure of the input" From ffda93567247e49ff5509aab029a3876dc64f70f Mon Sep 17 00:00:00 2001 From: Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> Date: Mon, 13 Feb 2023 21:13:39 +0100 Subject: [PATCH 2/7] Add snippets --- exercises/practice/pangram/.approaches/all/snippet.txt | 5 +++++ .../practice/pangram/.approaches/cross-off-all/snippet.txt | 3 +++ .../pangram/.approaches/cross-off-one-by-one/snippet.txt | 5 +++++ .../practice/pangram/.approaches/substructure/snippet.txt | 5 +++++ 4 files changed, 18 insertions(+) create mode 100644 exercises/practice/pangram/.approaches/all/snippet.txt create mode 100644 exercises/practice/pangram/.approaches/cross-off-all/snippet.txt create mode 100644 exercises/practice/pangram/.approaches/cross-off-one-by-one/snippet.txt create mode 100644 exercises/practice/pangram/.approaches/substructure/snippet.txt diff --git a/exercises/practice/pangram/.approaches/all/snippet.txt b/exercises/practice/pangram/.approaches/all/snippet.txt new file mode 100644 index 000000000..2a4414407 --- /dev/null +++ b/exercises/practice/pangram/.approaches/all/snippet.txt @@ -0,0 +1,5 @@ +isPangram :: String -> Bool +isPangram text = + all + (`elem` map toLower text) + ['a' .. 'z'] diff --git a/exercises/practice/pangram/.approaches/cross-off-all/snippet.txt b/exercises/practice/pangram/.approaches/cross-off-all/snippet.txt new file mode 100644 index 000000000..e9d87cf60 --- /dev/null +++ b/exercises/practice/pangram/.approaches/cross-off-all/snippet.txt @@ -0,0 +1,3 @@ +isPangram :: String -> Bool +isPangram text = + null (['a' .. 'z'] \\ map toLower text) diff --git a/exercises/practice/pangram/.approaches/cross-off-one-by-one/snippet.txt b/exercises/practice/pangram/.approaches/cross-off-one-by-one/snippet.txt new file mode 100644 index 000000000..31d91cc1c --- /dev/null +++ b/exercises/practice/pangram/.approaches/cross-off-one-by-one/snippet.txt @@ -0,0 +1,5 @@ +isPangram :: String -> Bool +isPangram = + any null + . scanl (flip delete) ['a' .. 'z'] + . map toLower diff --git a/exercises/practice/pangram/.approaches/substructure/snippet.txt b/exercises/practice/pangram/.approaches/substructure/snippet.txt new file mode 100644 index 000000000..a10a72fe1 --- /dev/null +++ b/exercises/practice/pangram/.approaches/substructure/snippet.txt @@ -0,0 +1,5 @@ +isPangram :: String -> Bool +isPangram = + (['a' .. 'z'] `isSubsequenceOf`) + . sort + . map toLower From 0b46205cac92ae0a3802c3ad7c1d8e124abedd31 Mon Sep 17 00:00:00 2001 From: Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> Date: Mon, 13 Feb 2023 21:55:11 +0100 Subject: [PATCH 3/7] Add config.json --- .../practice/pangram/.approaches/config.json | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 exercises/practice/pangram/.approaches/config.json diff --git a/exercises/practice/pangram/.approaches/config.json b/exercises/practice/pangram/.approaches/config.json new file mode 100644 index 000000000..2d38c01e3 --- /dev/null +++ b/exercises/practice/pangram/.approaches/config.json @@ -0,0 +1,45 @@ +{ + "introduction": { + "authors": [ + "MatthijsBlom" + ] + }, + "approaches": [ + { + "uuid": "98321284-dc12-4ba9-9ffa-e4b7c8b99271", + "slug": "all", + "title": "What it means to be a pangram", + "blurb": "Use all to check that all letters are present.", + "authors": [ + "MatthijsBlom" + ] + }, + { + "uuid": "902089fe-ab1f-4fb1-b7c1-02e50b5b06e2", + "slug": "substructure", + "title": "Is the alphabet contained in the input?", + "blurb": "Use isSubsequenceOf or isSubsetOf to check that the alphabet is part of the input.", + "authors": [ + "MatthijsBlom" + ] + }, + { + "uuid": "b130e9d5-4468-40c9-b78a-2c9b8f32ae9d", + "slug": "cross-off-all", + "title": "Cross off all letters at once", + "blurb": "Walking over the input, cross off letters of the alphabet as you go and finally check that none are left.", + "authors": [ + "MatthijsBlom" + ] + }, + { + "uuid": "07f33d97-d2f1-49e9-b23d-965e37749487", + "slug": "cross-off-one-by-one", + "title": "Cross off letters one by one", + "blurb": "Walking over the input, cross off letters of the alphabet one by one, continuously checking whether all have been removed already.", + "authors": [ + "MatthijsBlom" + ] + } + ] +} From 6e8a144e88c207f2b7998bcc54d3e0aff72afa49 Mon Sep 17 00:00:00 2001 From: Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> Date: Tue, 21 Feb 2023 20:43:03 +0100 Subject: [PATCH 4/7] Tweak language --- exercises/practice/pangram/.approaches/introduction.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/practice/pangram/.approaches/introduction.md b/exercises/practice/pangram/.approaches/introduction.md index f1fabd1cc..a29b9b5cb 100644 --- a/exercises/practice/pangram/.approaches/introduction.md +++ b/exercises/practice/pangram/.approaches/introduction.md @@ -29,9 +29,9 @@ isPangram text = all (`elem` map toLower text) ['a' .. 'z'] This solution is simply the definition of _pangram_ put into ~~words~~ code. A sentence is a pangram when `all` letters of the alphabet are present. -This solution iterates over the alphabet first and tolerates infinite input. +This approach iterates over the alphabet first and tolerates infinite input. However, it is inefficient for some inputs. -This can be remedied using `Set`, at the cost of losing tolerance for infinite input. +This can be remedied using `Set`, at the cost of losing tolerance of infinite input. [Read more about this approach][all]. @@ -46,12 +46,12 @@ isPangram = (['a' .. 'z'] `isSubsequenceOf`) . sort . map toLower This approach uses `isSubsequenceOf` or `isSubsetOf` to check that the alphabet forms a subsequence/subset of the input. Due to `sort` or `Set.fromList`, this approach does not tolerate infinite input. -It is efficient though: after sorting/accumulating, both the alphabet and the input are walked only once. +It is efficient though: after sorting/gathering, both the alphabet and the input are walked only once. [Read more about this approach][substructure]. -## Approach: cross off all characters at once +## Approach: cross off all characters in one go ```haskell isPangram :: String -> Bool From 12d7c4235b6ff20c936ba8b84c91288794aa4e8a Mon Sep 17 00:00:00 2001 From: Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> Date: Tue, 21 Feb 2023 23:27:44 +0100 Subject: [PATCH 5/7] Add approach: using `all` --- .../pangram/.approaches/all/content.md | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 exercises/practice/pangram/.approaches/all/content.md diff --git a/exercises/practice/pangram/.approaches/all/content.md b/exercises/practice/pangram/.approaches/all/content.md new file mode 100644 index 000000000..2aa555c92 --- /dev/null +++ b/exercises/practice/pangram/.approaches/all/content.md @@ -0,0 +1,146 @@ +# Express the definition of _pangram_ + +```haskell +isPangram :: String -> Bool +isPangram text = all (`elem` map toLower text) ['a' .. 'z'] +``` + +This solution is simply the definition of _pangram_ put into ~~words~~ code. +A sentence is a pangram when `all` letters of the alphabet are present. + +This approach iterates over the alphabet first and tolerates infinite input. +However, it is inefficient for some inputs. +This can be remedied using `Set`, at the cost of losing tolerance for infinite input. + + +## `any` & `all` + +`any` and `all` are **higher-order functions** that take a predicate (a function that produces a `Boolean`) and a list as arguments. +Both check whether elements of the list satisfy the predicate. +`any` produces `True` when there is _at least one_ element that satisfies the predicate, and `False` otherwise. +In contrast, `all` produces `True` only when _all_ elements satisfy the predicate. + +```haskell +-- >>> any even [1, 3, 5] -- no even numbers in this list +-- False +-- >>> any even [1 .. 5] -- at least one even number in this list +-- True +-- >>> all even [2, 4, 6] -- all numbers in this list are even +-- True +-- >>> all even [2 .. 6] -- not all numbers in this list are even +-- False +``` + +How do these work? + +~~~~exercism/advanced +To find they actual definitions of `any` and `all`, look up their documentation (for example through [Hoogle][hoogle]) and click on the «Source» link next to the type signature. + +The definitions of `any` and `all` look kinda complicated! +They are this way so that they can also be used on types other than lists, such as `Set`, `Map`, and `Tree`. +Specifically, they can be used on any type that is an instance of the `Foldable` type class. + +In the source code, you can click on names to jump to their definitions. +This doesn't really work for `foldMap` here though: it sends you to its default (general) implementation, but here we need its implementation for lists specifically. +To find that code, navigate to the documentation of `Foldable`, look up `[]` in the list of «Instances», and click «Source». + +It might be hard to see – even after lots of clicking through to definitions – but these definitions of `any` and `all` work essentially the same way as the one outlined here below. + +[hoogle]: https://hoogle.haskell.org/ "Hoogle" +~~~~ + +Here is a possible definition of `any`: + +```haskell +or :: [Bool] -> Bool +or = foldr (||) True + +any p = or . map p +``` + +And this is how it evaluates: + +```haskell +_ = any (2 <) [1..] + == (or . map (2 <)) [1..] + == or ( map (2 <) [1..] ) + == foldr (||) True ( map (2 <) [1..] ) + -- look at next element of the list + == foldr (||) True ( 2 < 1 : map (2 <) [2..] ) + == 2 < 1 || foldr (||) True ( map (2 <) [2..] ) + == False || foldr (||) True ( map (2 <) [2..] ) + == foldr (||) True ( map (2 <) [2..] ) + -- look at next element of the list + == foldr (||) True ( 2 < 2 : map (2 <) [3..] ) + == 2 < 2 || foldr (||) True ( map (2 <) [3..] ) + == False || foldr (||) True ( map (2 <) [3..] ) + == foldr (||) True ( map (2 <) [3..] ) + -- look at next element of the list + == foldr (||) True ( 2 < 3 : map (2 <) [4..] ) + == 2 < 3 || foldr (||) True ( map (2 <) [4..] ) + == True || foldr (||) True ( map (2 <) [4..] ) + -- (||) short-circuits + == True +``` + +As you can see, evaluation terminates as soon as a number larger than `2` is found. +And thanks to laziness, `any` and `all` even work on infinite lists! +Provided the answer can be determined after looking at finitely many elements, that is. + + +## In this approach + +We use `all` to check that all the letters of the alphabet are present in the input. + +The predicate that we use is ``(`elem` map toLower text)``, which is an example of an [operator section][operator-section]. +For any given letter, it checks that it is an element of the normalized input. + +For some infinite inputs, such as `['a' .. 'z'] ++ repeat '!'`, this approach will be able to produce an answer in finite time. +If all letters are present, `elem` will find them all in finite time and `isPangram` will evaluate to `True`. +However, if any letter is absent, `elem` will never stop searching for it, and `isPangram` will never terminate. + +`elem` performs a linear search: it checks elements one by one, in order. +Therefore, it can take a very long time to find values that occur only very deep into a list. +For example, for `replicate 1_000_000_000 '?' ++ ['x']` some 1.000.000.001 checks are required before it can be determined that `'x'` is an element of that list. + +Having to look at many elements of very long lists is unavoidable. +However, this approach uses `elem` _26 times_, good for 26 separate linear searches, each of which might take long to complete. +This can certainly add up! + +This is where `Set` comes in. +`fromList` allows gathering the entire input in one go. +Thereafter, checking for membership with `member` is very cheap, by the nature of `Set`s. + +```haskell +import Data.Set (fromList, member) + +isPangram :: String -> Bool +isPangram text = all (`member` characters) alphabet + where + alphabet = ['a' .. 'z'] + characters = fromList (map toLower text) +``` + +Because `fromList` needs to walk the list entirely before it can produce the `Set`, this version no longer works on infinite input. + +Converting the alphabet to a `Set` wouldn't buy us improved performance. +There would be exactly the same number of elements either way. +However, it would allow us to use `isSubsetOf`, which is equivalent to the above use of `all` but might be preferred for aesthetic reasons. + +```haskell +import Data.Set (fromDistinctAscList, fromList, isSubsetOf) + +isPangram :: String -> Bool +isPangram text = characters `isSubsetOf` alphabet + where + alphabet = fromDistinctAscList ['a' .. 'z'] + characters = fromList (map toLower text) +``` + +Here `fromDistinctAscList` instead of `fromList` is used because it is more efficient, which is possible because `['a' .. 'z']` is already sorted and does not contain duplicates. +The argument of `isPangram` is not guaranteed to have these advantageous properties, so we must use `fromList`. + + +[operator-section]: + https://wiki.haskell.org/Section_of_an_infix_operator + "Haskell wiki: Section of an infix operator" From 1486afd8287b16317ab4bdc2bdf13048fd59305c Mon Sep 17 00:00:00 2001 From: Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> Date: Fri, 24 Feb 2023 00:42:37 +0100 Subject: [PATCH 6/7] Minor corrections --- exercises/practice/pangram/.approaches/all/content.md | 2 +- exercises/practice/pangram/.approaches/introduction.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/practice/pangram/.approaches/all/content.md b/exercises/practice/pangram/.approaches/all/content.md index 2aa555c92..cb2f41183 100644 --- a/exercises/practice/pangram/.approaches/all/content.md +++ b/exercises/practice/pangram/.approaches/all/content.md @@ -131,7 +131,7 @@ However, it would allow us to use `isSubsetOf`, which is equivalent to the above import Data.Set (fromDistinctAscList, fromList, isSubsetOf) isPangram :: String -> Bool -isPangram text = characters `isSubsetOf` alphabet +isPangram text = alphabet `isSubsetOf` characters where alphabet = fromDistinctAscList ['a' .. 'z'] characters = fromList (map toLower text) diff --git a/exercises/practice/pangram/.approaches/introduction.md b/exercises/practice/pangram/.approaches/introduction.md index a29b9b5cb..addd6495c 100644 --- a/exercises/practice/pangram/.approaches/introduction.md +++ b/exercises/practice/pangram/.approaches/introduction.md @@ -45,7 +45,7 @@ isPangram = (['a' .. 'z'] `isSubsequenceOf`) . sort . map toLower This approach uses `isSubsequenceOf` or `isSubsetOf` to check that the alphabet forms a subsequence/subset of the input. -Due to `sort` or `Set.fromList`, this approach does not tolerate infinite input. +Due to `sort` or `fromList`, this approach does not tolerate infinite input. It is efficient though: after sorting/gathering, both the alphabet and the input are walked only once. [Read more about this approach][substructure]. From ab14ba910dd7ff5601a10e433b39d5435f1fc360 Mon Sep 17 00:00:00 2001 From: Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> Date: Fri, 24 Feb 2023 01:04:42 +0100 Subject: [PATCH 7/7] Add approach: look for substructure --- .../.approaches/substructure/content.md | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 exercises/practice/pangram/.approaches/substructure/content.md diff --git a/exercises/practice/pangram/.approaches/substructure/content.md b/exercises/practice/pangram/.approaches/substructure/content.md new file mode 100644 index 000000000..597427b49 --- /dev/null +++ b/exercises/practice/pangram/.approaches/substructure/content.md @@ -0,0 +1,201 @@ +# Check that the alphabet is a substructure of the input + +```haskell +isPangram :: String -> Bool +isPangram = (['a' .. 'z'] `isSubsequenceOf`) . sort . map toLower +``` + +This approach is to check that the alphabet is some kind of _substructure_ of the input. +What this should mean can be interpreted in various ways. +Three possible interpretations are described below. + + +## Interpretation: as a sequence of characters + +A [subsequence][wikipedia-subsequence] is a sequence all of whose elements are present in another sequence, in the same order. +That is, a sequence is a subsequence if it is equal to another sequence with possibly some elements removed. + +For example, «2, 3, 5» is a subsequence of «1, 2, 3, 4, 5», but «4, 2» is not. + +```haskell +-- >>> [2, 3, 5] `isSubsequenceOf` [1 .. 5] +-- True +-- >>> [4, 2] `isSubsequenceOf` [1 .. 5] +-- False +``` + +As its name suggests, `isSubsequenceOf` checks that a list represents a subsequence of another list. +It does so efficiently – in one pass – and can even deal with infinite lists sometimes: + +```haskell +-- >>> [5, 7 .. 20] `isSubsequenceOf` [0 ..] +-- True +-- >>> [0 ..] `isSubsequenceOf` [5, 7 .. 20] +-- False +``` + +It doesn't work when the finite list is _not_ a subsequence of the infinite one though. +Then `isSubsequenceOf` will never stop searching for the first value that is present on the left but not on the right. +That is, ``[0] `isSubsequenceOf` [1 ..]`` would never finish evaluating. + +Similarly, it cannot deal with _two_ infinite lists. +You wouldn't be able to either: you would never be done searching for the next matching element! + +~~~~exercism/advanced +To see `subsequence`'s source code, look up its documentation (for example through [Hoogle][hoogle]) and click on the «Source» link next to the type signature. + +[hoogle]: https://hoogle.haskell.org/ "Hoogle" +~~~~ + +`isSubsequenceOf` can be used to check that all the letters of the alphabet are present in the input. +It cannot by used simply by itself though: + +```haskell +alphabet = ['a' .. 'z'] +text = reverse alphabet + +-- >>> alphabet `isSubsequenceOf` text +-- False +``` + +The letters in the input should be put into the right order first. +This can be achieved with `sort`. + +```haskell +-- >>> sort "cadbe" +-- "abcde" +``` + +Together, `isSubsequenceOf` and `sort` provide a solution to this exercise. + +```haskell +isPangram :: String -> Bool +isPangram text = alphabet `isSubsequenceOf` characters + where + alphabet = ['a' .. 'z'] + characters = sort (map toLower text) +``` + +`isSubsequenceOf` would definitely be able to deal with infinitely long `text`, but `sort` cannot. +Hence, this solution does not tolerate infinite input. + + +## Interpretation: as a set of characters + +A list either does contain a certain value, or it does not. +This exercise is all about characters being or not being present in strings. +This suggests viewing these strings as [sets][wikipedia-set-adt] of characters. +For this, the `Set` data type can be used. + +~~~~exercism/note +The `Set` data type is not provided by `base`, the standard library. +Instead, it lives in the `containers` package. +To use it in your own solutions, add `containers` to the list of dependencies in the `package.yaml` file, like so: + +```yaml +dependencies: + - base + - containers # 👈 +``` +~~~~ + +Lists can be converted into `Set`s using the `fromList` function. + +```haskell +someIntegers :: Set Integer +someIntegers = fromList [3, 3, 2, 3, 1, 2] + +-- >>> elems someIntegers +-- [1,2,3] +``` + +`isSubsetOf` can be used to check that one `Set`'s elements are all present in another `Set`. + +```haskell +s, t, r :: Set Integer +s = fromList [3, 5, 2] +t = fromList [1 .. 5] +r = fromList [6, 4] + +-- >>> s `isSubsetOf` t +-- True +-- >>> r `isSubsetOf` t +-- False +``` + +This is precisely what is required in this exercise. + +```haskell +isPangram :: String -> Bool +isPangram text = alphabet `isSubsetOf` characters + where + alphabet = fromDistinctAscList ['a' .. 'z'] + characters = fromList (map toLower text) +``` + +Here `fromDistinctAscList` instead of `fromList` is used because it is more efficient, which is possible because `['a' .. 'z']` is already sorted and does not contain duplicates. +The argument of `isPangram` is not guaranteed to have these advantageous properties, so we must use `fromList`. + + +## Interpretation: as a multiset of characters + +Where a set is concerned with _whether_ a certain value is present, a [multiset][wikipedia-multiset-adt] is concerned with _how often_ it is present. + +The `MultiSet` data type is very similar to `Set`, but is more general in that it keeps track of how many copies of the same value it contains. +In a sense, a `Set` is just a `MultiSet` in which every value occurs at most once. + +~~~~exercism/note +`MultiSet` lives in the `multiset` package, which you need to add to the list of dependencies in `package.yaml` in order to use it. +~~~~ + +~~~~exercism/note +In other language, multisets are also known as `Bag`s, and as `Counter` in Python. +~~~~ + +```haskell +letters :: MultiSet Char +letters = fromList "abbcccdddd" + +-- >>> toOccurList letters +-- [('a',1),('b',2),('c',3),('d',4)] +``` + +For this exercise, `MultiSet`'s extra capabilities aren't useful. +However, in other situations they often are, and multisets are a useful tool to have in your toolbox. + +Like `Set`s, `MultiSet`s have a notion of _subset_. +A multiset is a subset when all its elements are present at least as often in another multiset. + +```haskell +s, t :: MultiSet Char +s = fromList "a bb ccc dddd" +t = fromList "dddddd aa cccc ee bb" + +-- >>> s `isSubsetOf` t +-- True +-- >>> t `isSubsetOf` s +-- False +``` + +Again, this is precisely what is required in this exercise: a phrase is a pangram if it contains every letter at least as often as the alphabet. + +```haskell +isPangram :: String -> Bool +isPangram text = alphabet `isSubsetOf` characters + where + alphabet = fromDistinctAscList ['a' .. 'z'] + characters = fromList (map toLower text) +``` + +The code is the same as for the above version using `Set`, because none of `MultiSet`'s extra capabilities are needed. + + +[wikipedia-multiset-adt]: + https://en.wikipedia.org/wiki/Set_(abstract_data_type)#Multiset + "Wikipedia: Multiset (abstract data type)" +[wikipedia-set-adt]: + https://en.wikipedia.org/wiki/Set_(abstract_data_type) + "Wikipedia: Set (abstract data type)" +[wikipedia-subsequence]: + https://en.wikipedia.org/wiki/Subsequence + "Wikipedia: Subsequence"