Skip to content

Commit 95ba326

Browse files
Merge #828: Nix.Builtins: replaceStrings: refactor
Performance optimization. Readability.
2 parents 9fb1df4 + c9c885d commit 95ba326

File tree

1 file changed

+85
-44
lines changed

1 file changed

+85
-44
lines changed

src/Nix/Builtins.hs

Lines changed: 85 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import qualified Data.Aeson as A
3838
import Data.Align ( alignWith )
3939
import Data.Array
4040
import Data.Bits
41+
import Data.Bool ( bool )
4142
import Data.ByteString ( ByteString )
4243
import qualified Data.ByteString as B
4344
import Data.ByteString.Base16 as Base16
@@ -923,56 +924,96 @@ genericClosure = fromValue @(AttrSet (NValue t f m)) >=> \s ->
923924
WValue j : _ -> checkComparable k' j
924925
fmap (t :) <$> go op (ts <> ys) (S.insert (WValue k') ks)
925926

927+
-- | Takes:
928+
-- 1. List of strings to match.
929+
-- 2. List of strings to replace corresponding match occurance. (arg 1 & 2 lists matched by index)
930+
-- 3. String to process
931+
-- -> returns the string with requested replacements.
932+
--
933+
-- Example:
934+
-- builtins.replaceStrings ["ll" "e"] [" " "i"] "Hello world" == "Hi o world".
926935
replaceStrings
927936
:: MonadNix e t f m
928937
=> NValue t f m
929938
-> NValue t f m
930939
-> NValue t f m
931940
-> m (NValue t f m)
932-
replaceStrings tfrom tto ts = fromValue (Deeper tfrom) >>= \(nsFrom :: [NixString]) ->
933-
fromValue (Deeper tto) >>= \(nsTo :: [NixString]) ->
934-
fromValue ts >>= \(ns :: NixString) -> do
935-
let from = fmap stringIgnoreContext nsFrom
936-
when (length nsFrom /= length nsTo)
937-
$ throwError
938-
$ ErrorCall
939-
$ "'from' and 'to' arguments to 'replaceStrings'"
940-
<> " have different lengths"
941-
let
942-
lookupPrefix s = do
943-
(prefix, replacement) <- find ((`Text.isPrefixOf` s) . fst)
944-
$ zip from nsTo
945-
let rest = Text.drop (Text.length prefix) s
946-
pure (prefix, replacement, rest)
947-
finish b =
948-
makeNixString (LazyText.toStrict $ Builder.toLazyText b)
949-
go orig result ctx = case lookupPrefix orig of
950-
Nothing -> case Text.uncons orig of
951-
Nothing -> finish result ctx
952-
Just (h, t) -> go t (result <> Builder.singleton h) ctx
953-
Just (prefix, replacementNS, rest) ->
954-
let replacement = stringIgnoreContext replacementNS
955-
newCtx = NixString.getContext replacementNS
956-
in case prefix of
957-
"" -> case Text.uncons rest of
958-
Nothing -> finish
959-
(result <> Builder.fromText replacement)
960-
(ctx <> newCtx)
961-
Just (h, t) -> go
962-
t
963-
(mconcat
964-
[ result
965-
, Builder.fromText replacement
966-
, Builder.singleton h
967-
]
968-
)
969-
(ctx <> newCtx)
970-
_ -> go rest
971-
(result <> Builder.fromText replacement)
972-
(ctx <> newCtx)
973-
toValue
974-
$ go (stringIgnoreContext ns) mempty
975-
$ NixString.getContext ns
941+
replaceStrings tfrom tto ts =
942+
do
943+
-- NixStrings have context - remember
944+
(fromKeys :: [NixString]) <- fromValue (Deeper tfrom)
945+
(toVals :: [NixString]) <- fromValue (Deeper tto)
946+
(string :: NixString ) <- fromValue ts
947+
948+
when (length fromKeys /= length toVals) $ throwError $ ErrorCall "builtins.replaceStrings: Arguments `from`&`to` construct a key-value map, so the number of their elements must always match."
949+
950+
let
951+
-- 2021-02-18: NOTE: if there is no match - the process does not changes the context, simply slides along the string.
952+
-- So it isbe more effective to pass the context as the first argument.
953+
-- And moreover, the `passOneCharNgo` passively passes the context, to context can be removed from it and inherited directly.
954+
-- Then the solution would've been elegant, but the Nix bug prevents elegant implementation.
955+
go ctx input output =
956+
maybe
957+
-- Passively pass the chars
958+
passOneChar
959+
replace
960+
maybePrefixMatch
961+
962+
where
963+
-- When prefix matched something - returns (match, replacement, reminder)
964+
maybePrefixMatch :: Maybe (Text, NixString, Text)
965+
maybePrefixMatch = formMatchReplaceTailInfo <$> find ((`Text.isPrefixOf` input) . fst) fromKeysToValsMap
966+
where
967+
formMatchReplaceTailInfo = (\(m, r) -> (m, r, Text.drop (Text.length m) input))
968+
969+
fromKeysToValsMap = zip (fmap stringIgnoreContext fromKeys) toVals
970+
971+
-- Not passing args => It is constant that gets embedded into `go` => It is simple `go` tail recursion
972+
passOneChar =
973+
maybe
974+
(finish ctx output) -- The base case - there is no chars left to process -> finish
975+
(\(c, i) -> go ctx i (output <> Builder.singleton c)) -- If there are chars - pass one char & continue
976+
(Text.uncons input) -- chip first char
977+
978+
-- 2021-02-18: NOTE: rly?: toStrict . toLazyText
979+
-- Maybe `text-builder`, `text-show`?
980+
finish ctx output = makeNixString (LazyText.toStrict $ Builder.toLazyText output) ctx
981+
982+
replace (key, replacementNS, unprocessedInput) = replaceWithNixBug unprocessedInput updatedOutput
983+
984+
where
985+
replaceWithNixBug =
986+
bool
987+
(go updatedCtx) -- tail recursion
988+
-- Allowing match on "" is a inherited bug of Nix,
989+
-- when "" is checked - it always matches. And so - when it checks - it always insers a replacement, and then process simply passesthrough the char that was under match.
990+
--
991+
-- repl> builtins.replaceStrings ["" "e"] [" " "i"] "Hello world"
992+
-- " H e l l o w o r l d "
993+
-- repl> builtins.replaceStrings ["ll" ""] [" " "i"] "Hello world"
994+
-- "iHie ioi iwioirilidi"
995+
-- 2021-02-18: NOTE: There is no tests for this
996+
bugPassOneChar -- augmented recursion
997+
isNixBugCase
998+
999+
isNixBugCase = key == mempty
1000+
1001+
updatedOutput = output <> replacement
1002+
updatedCtx = ctx <> replacementCtx
1003+
1004+
replacement = Builder.fromText $ stringIgnoreContext replacementNS
1005+
replacementCtx = NixString.getContext replacementNS
1006+
1007+
-- The bug modifies the content => bug demands `pass` to be a real function =>
1008+
-- `go` calls `pass` function && `pass` calls `go` function
1009+
-- => mutual recusion case, so placed separately.
1010+
bugPassOneChar input output =
1011+
maybe
1012+
(finish updatedCtx output) -- The base case - there is no chars left to process -> finish
1013+
(\(c, i) -> go updatedCtx i (output <> Builder.singleton c)) -- If there are chars - pass one char & continue
1014+
(Text.uncons input) -- chip first char
1015+
1016+
toValue $ go (NixString.getContext string) (stringIgnoreContext string) mempty
9761017

9771018
removeAttrs
9781019
:: forall e t f m

0 commit comments

Comments
 (0)