|
| 1 | +# Introduction |
| 2 | + |
| 3 | +This problem requires both |
| 4 | + |
| 5 | +- validating that all input characters validly denote DNA nucleobases, and |
| 6 | +- producing these DNA nucleobases' corresponding RNA nucleobases. |
| 7 | + |
| 8 | +The first below listed approach has these tasks performed separately. |
| 9 | +The other ones combine them in a single pass, in progressively more succinct ways. |
| 10 | + |
| 11 | + |
| 12 | +## Approach: validate first |
| 13 | + |
| 14 | +```haskell |
| 15 | +toRNA :: String -> Either Char String |
| 16 | +toRNA dna = |
| 17 | + case find (`notElem` "GCTA") dna of |
| 18 | + Nothing -> Right (map transcribe dna) |
| 19 | + Just c -> Left c |
| 20 | + where |
| 21 | + transcribe = \case |
| 22 | + 'G' -> 'C' |
| 23 | + 'C' -> 'G' |
| 24 | + 'T' -> 'A' |
| 25 | + 'A' -> 'U' |
| 26 | +``` |
| 27 | + |
| 28 | +First search for the first invalid nucleobase. |
| 29 | +If you find one, return it. |
| 30 | +If all are valid, transcribe the entire strand in one go using `map`. |
| 31 | + |
| 32 | +This approach has the input walked twice. |
| 33 | +Other approaches solve this problem in one pass. |
| 34 | + |
| 35 | +This solution deals with nucleobases twice: first when validating, and again when transcribing. |
| 36 | +Ideally, nucleobases are dealt with in only one place in the code. |
| 37 | + |
| 38 | +[Read more about this approach][validate-first]. |
| 39 | + |
| 40 | + |
| 41 | +## Approach: a single pass using only elementary operations |
| 42 | + |
| 43 | +```haskell |
| 44 | +toRNA :: String -> Either Char String |
| 45 | +toRNA [] = Right [] |
| 46 | +toRNA (n : dna) = case transcribe n of |
| 47 | + Nothing -> Left n |
| 48 | + Just n' -> case toRNA dna of |
| 49 | + Left c -> Left c |
| 50 | + Right rna -> Right (n' : rna) |
| 51 | + |
| 52 | +transcribe :: Char -> Maybe Char |
| 53 | +transcribe = \case |
| 54 | + 'G' -> Just 'C' |
| 55 | + 'C' -> Just 'G' |
| 56 | + 'T' -> Just 'A' |
| 57 | + 'A' -> Just 'U' |
| 58 | + _ -> Nothing |
| 59 | +``` |
| 60 | + |
| 61 | +This solution combines validation and transcription in a single list traversal. |
| 62 | +It is _elementary_ in the sense that it employs no abstractions: it uses only constructors (`[]`, `(:)`, `Nothing`, `Just`, `Left`, `Right`) and pattern matching, and no predefined functions at all. |
| 63 | + |
| 64 | +Some of the code patterns used in this solution are very common, and were therefore abstracted into standard library functions. |
| 65 | +The approaches listed below show how much these functions can help to concisely express this approach's logic. |
| 66 | + |
| 67 | +[Read more about this approach][elementary]. |
| 68 | + |
| 69 | + |
| 70 | +## Approach: use `do`-notation |
| 71 | + |
| 72 | +```haskell |
| 73 | +toRNA :: String -> Either Char String |
| 74 | +toRNA [] = pure [] |
| 75 | +toRNA (n : dna) = do |
| 76 | + n' <- transcribe n |
| 77 | + rna <- toRNA dna |
| 78 | + pure (n' : rna) |
| 79 | + |
| 80 | +transcribe :: Char -> Either Char Char |
| 81 | +transcribe = \case |
| 82 | + 'G' -> Right 'C' |
| 83 | + 'C' -> Right 'G' |
| 84 | + 'T' -> Right 'A' |
| 85 | + 'A' -> Right 'U' |
| 86 | + c -> Left c |
| 87 | +``` |
| 88 | + |
| 89 | +The [elementary solution][elementary] displays a common pattern that can equivalently be expressed using the common monadic `>>=` combinator and its `do`-notation [syntactic sugar][wikipedia-syntactic-sugar]. |
| 90 | + |
| 91 | +[Read more about this approach][do-notation]. |
| 92 | + |
| 93 | + |
| 94 | +## Approach: use `Functor`/`Applicative` combinators |
| 95 | + |
| 96 | +```haskell |
| 97 | +toRNA :: String -> Either Char String |
| 98 | +toRNA [] = pure [] |
| 99 | +toRNA (n : dna) = (:) <$> transcribe n <*> toRNA dna |
| 100 | + |
| 101 | +transcribe :: Char -> Either Char Char |
| 102 | +transcribe = \case |
| 103 | + 'G' -> Right 'C' |
| 104 | + 'C' -> Right 'G' |
| 105 | + 'T' -> Right 'A' |
| 106 | + 'A' -> Right 'U' |
| 107 | + c -> Left c |
| 108 | +``` |
| 109 | + |
| 110 | +The [elementary solution][elementary] displays a number of common patterns. |
| 111 | +As demonstrated by the [`do` notation solution][do-notation], these can be expressed with the `>>=` operator. |
| 112 | +However, the full power of `Monad` is not required. |
| 113 | +The same logic can also be expressed using common functorial combinators such as `fmap`/`<$>` and `<*>`. |
| 114 | + |
| 115 | +[Read more about this approach][functorial-combinators]. |
| 116 | + |
| 117 | + |
| 118 | +## Approach: use `traverse` |
| 119 | + |
| 120 | +```haskell |
| 121 | +toRNA :: String -> Either Char String |
| 122 | +toRNA = traverse $ \case |
| 123 | + 'G' -> Right 'C' |
| 124 | + 'C' -> Right 'G' |
| 125 | + 'T' -> Right 'A' |
| 126 | + 'A' -> Right 'U' |
| 127 | + n -> Left n |
| 128 | +``` |
| 129 | + |
| 130 | +As it turns out, the [solution that uses functorial combinators][functorial-combinators] closely resembles the definition of `traverse` for lists. |
| 131 | +In fact, through a series of rewritings it can be shown to be equivalent. |
| 132 | + |
| 133 | +[Read more about this approach][traverse]. |
| 134 | + |
| 135 | + |
| 136 | +## General guidance |
| 137 | + |
| 138 | +### Language extensions |
| 139 | + |
| 140 | +For various reasons, some of GHC's features are locked behind switches known as _language extensions_. |
| 141 | +You can enable these by putting so-called _language pragmas_ at the top of your file: |
| 142 | + |
| 143 | +```haskell |
| 144 | +-- This 👇 is a language pragma |
| 145 | +{-# LANGUAGE LambdaCase #-} |
| 146 | + |
| 147 | +module DNA (toRNA) where |
| 148 | + |
| 149 | +{- |
| 150 | + The rest of your code here |
| 151 | +-} |
| 152 | +``` |
| 153 | + |
| 154 | + |
| 155 | +#### `LambdaCase` |
| 156 | + |
| 157 | +Consider the following possible definition of `map`. |
| 158 | + |
| 159 | +```haskell |
| 160 | +map f xs = case xs of |
| 161 | + [] -> [] |
| 162 | + x : xs' -> f x : map xs' |
| 163 | +``` |
| 164 | + |
| 165 | +Here, a parameter `xs` is introduced only to be immediately pattern matched against, after which it is never used again. |
| 166 | + |
| 167 | +Coming up with good names for such throwaway variables can be tedious and hard. |
| 168 | +The `LambdaCase` extension allows us to avoid having to by providing an extra bit of [syntactic sugar][wikipedia-syntactic-sugar]: |
| 169 | + |
| 170 | +```haskell |
| 171 | +f = \case { } |
| 172 | +-- is syntactic sugar for / an abbreviation of |
| 173 | +f = \x -> case x of { } |
| 174 | +``` |
| 175 | + |
| 176 | +The above definition of `map` can equivalently be written as |
| 177 | + |
| 178 | +```haskell |
| 179 | +map f = \case |
| 180 | + [] -> [] |
| 181 | + x : xs -> f x : map f xs |
| 182 | +``` |
| 183 | + |
| 184 | + |
| 185 | +[do-notation]: |
| 186 | + https://exercism.org/tracks/haskell/exercises/rna-transcription/approaches/do-notation |
| 187 | + "Approach: use do-notation" |
| 188 | +[elementary]: |
| 189 | + https://exercism.org/tracks/haskell/exercises/rna-transcription/approaches/elementary |
| 190 | + "Approach: a single pass using only elementary operations" |
| 191 | +[functorial-combinators]: |
| 192 | + https://exercism.org/tracks/haskell/exercises/rna-transcription/approaches/functorial-combinators |
| 193 | + "Approach: use Functor/Applicative combinators" |
| 194 | +[traverse]: |
| 195 | + https://exercism.org/tracks/haskell/exercises/rna-transcription/approaches/traverse |
| 196 | + "Approach: use traverse" |
| 197 | +[validate-first]: |
| 198 | + https://exercism.org/tracks/haskell/exercises/rna-transcription/approaches/validate-first |
| 199 | + "Approach: validate first" |
| 200 | + |
| 201 | + |
| 202 | +[wikipedia-syntactic-sugar]: |
| 203 | + https://en.wikipedia.org/wiki/Syntactic_sugar |
| 204 | + "Wikipedia: Syntactic sugar" |
0 commit comments