7
$\begingroup$

Let's say I have a piece of code:

Hold[{code1,
      "asdad " <> ToString[testa] <> " adsd " <> ToString[testb],
      code2}] (*MWE ofc*)

which I want to convert. Each StringJoin[...] should be replaced so I will get:

Hold[{code1, StringForm["asdad `` adsd ``", testa, testb], code2}]

I have an answer but maybe one may show shorter approach:

Hold[{code1, "asdad " <> ToString[testa] <> " adsd " <> ToString[testb], code2}
    ] /. HoldPattern[StringJoin[x__]
                    ] :> RuleCondition@(
          StringForm[StringJoin @@ (Hold[x] /. _ToString :> "``"), 
                     ##] & @@ Cases[Hold[x], HoldPattern[ToString[z_]] :> z]
          ) // InputForm
 Hold[{code1, StringForm["asdad `` adsd ``", testa, testb], code2}]

Edit: This solution is not perfect. It evauates testa and testb, does not matter in my case but for generality let's assume they may not be evaluated. Also, referring to first of Mr.Wizard's suggestions: StringJoin expressions may appear or different levels too.

$\endgroup$
7
  • 1
    $\begingroup$ Should all StringJoin objects be replaced, or only those at level 2, or only certain ones by position? $\endgroup$
    – Mr.Wizard
    Jan 17, 2014 at 13:54
  • $\begingroup$ @Mr.Wizard Let's say we don't know on which level they may appear. But if you have neat solution for the simpler case, I'm lookng forward seeing it too. $\endgroup$
    – Kuba
    Jan 17, 2014 at 13:57
  • $\begingroup$ I was trying to solve this on my own, without reading your solution, but I struggled to get evaluation correct so I looked at your method to see how you had solved it. I discovered that your code does not work properly: testa and testb get evaluated. Is that acceptable? $\endgroup$
    – Mr.Wizard
    Jan 17, 2014 at 14:22
  • $\begingroup$ I think I'm remembering something. Please tell me, what are the Attributes of StringForm on your system? $\endgroup$
    – Mr.Wizard
    Jan 17, 2014 at 14:34
  • 1
    $\begingroup$ @Mr.Wizard only Protected. Hmm, it seems they are, I've missed that because it does not make a difference for my purposes. $\endgroup$
    – Kuba
    Jan 17, 2014 at 14:36

3 Answers 3

11
$\begingroup$

I think that in general, for tasks like this one, tricks like Trott-Strzebonski technique are not the best way, and one really needs expression parsers, which are may be not shorter, but more readable and more extensible. Here is a possible one for your problem:

ClearAll[convert];
SetAttributes[convert, {HoldAll}];
convert[x_List] := Map[convert, Unevaluated[x]];
convert[Hold[{pieces___}]] :=
   (Hold[#] &[convert[{pieces}]]) /. convert[x_] :> x;
convert[s_StringJoin] := convertSJ[s];

where the specific converter for StringJoin is:

ClearAll[convertSJ];
SetAttributes[convertSJ, HoldAll];
convertSJ[s_StringJoin] := convertSJ[s, {}];
convertSJ[StringJoin[prev__String, ToString[x_], rest___], {accum___}] :=
   convertSJ[StringJoin[prev, " `` ", rest], {accum, x}];
convertSJ[s_StringJoin, {accum___}] := 
   With[{st = s}, convert[StringForm[st, accum]]];

So that

convert[
  Hold[{code1, "asdad " <> ToString[testa] <> " adsd " <> ToString[testb], code2}]
]

(* Hold[{code1, StringForm["asdad  ``  adsd  `` ", testa, testb], code2}] *)
$\endgroup$
18
  • $\begingroup$ I was about to ask "why" but then I remembered we've been over this before. Frankly I still don't agree, as that convert with all its cases seems more confusing to me than a single replacement rule that caries out a specific action. "Agree to disagree" I guess. $\endgroup$
    – Mr.Wizard
    Jan 17, 2014 at 15:53
  • $\begingroup$ @Mr.Wizard The big advantage of convert is that its rules are composable, which means that, for more complex expressions, it will automatically dispatch to a right rule for a given part, and then combine them all together again. The difference in power between plain rule-based approach and the one based on parsers is pretty much similar to a difference between a parser and a regexp, for string parsing (with a caveat that patterns in M are more powerful than regexps). The more complex the original expression and the transformation rules, the more this difference will show. $\endgroup$ Jan 17, 2014 at 16:02
  • $\begingroup$ @Mr.Wizard Another reason why I prefer this approach is that in every single rule in convert, I can focus on only what this particular rule should do. In a single rule a-la your solution or others, all these transformations are coupled together by the evaluation control constructs, while in my approach evaluation control does not couple the transformation together. If I want to change that rule later, for example, I don't have to care what else such a change might break. $\endgroup$ Jan 17, 2014 at 16:05
  • 1
    $\begingroup$ @Mr.Wizard Yes, I still think that the parser approach is still superior even in that case, because I find it easier to understand. In most (but not all) cases, I started to view TZ trick as a hack, and when you need something more complicated that what TZ gives out of the box (in terms of evaluation), you need more hacks to make it all work without evaluation leaks. The parser approach is straight-forward: you descend down the expression, transform, and then collect the expression back, with evaluation control taken care of explicitly in each case. $\endgroup$ Jan 17, 2014 at 16:16
  • 1
    $\begingroup$ You have almost convinced me but I still have trouble finding this easy to read. Would you consider posting a self Q&A with a graphical illustration of a simple parser? I think I lack a mental image that allows me to "see" this operation. I hope you understand what I am trying to say. $\endgroup$
    – Mr.Wizard
    Jan 17, 2014 at 16:27
1
$\begingroup$

This is what I came up with. Better? I don't know.

convert =
 Block[{StringForm},
   SetAttributes[StringForm, HoldRest];
   # /. sj_StringJoin :> RuleCondition[
      Reap[Unevaluated[sj] /. (t : ToString)[x_] :> (Sow[Hold@x]; " `` "), _, 
        Join @@ #2 &] /. {s_, {_[ex__]}} :> StringForm[s, ex]]
 ] &;

Test:

start = Hold[{code1, "asdad " <> ToString[testa] <> " adsd " <> ToString[testb], code2}];
{code1, code2} = {0, 0};
testa := 2 + 2
testb := Print["!"]

convert @ start // InputForm
Hold[{code1, StringForm["asdad  ``  adsd  `` ", testa, testb], code2}]

If we don't need to prevent evaluation of testa and testb, which Kuba's code does not do, we can simplify this considerably:

rule = sj_StringJoin :>
  RuleCondition[
   StringForm[#, Sequence @@ #2[[1]]] & @@
    Reap[Unevaluated[sj] /. ToString :> ((Sow[#]; " `` ") &)]
  ];

start /. rule // InputForm

During evaluation of In[]:= !

Hold[{code1, StringForm["asdad  ``  adsd  `` ", 4, Null], code2}]
$\endgroup$
1
  • $\begingroup$ This attribute is quite nice, I'm struggling with injection of unevaluated sequence in different way but I got stuck now. :p p.s. to many whitespaces: " `` " -> "``". Thanks again. $\endgroup$
    – Kuba
    Jan 18, 2014 at 0:24
1
$\begingroup$

An embedded Trott-Strzebonski method-approach. rep[expr, held, from -> to, f] works by:

  • holding held symbol (like ToString);
  • replacing symbol from with to (like StringJoin -> StringForm), partially evaluating arguments that are not held...
  • ...applying function f to arguments not held in from.

It leaves code... and test... parts unevaluated, es expected.

rep[expr_, held_, from_ -> to_, f_] := Block[{z}, 
   expr //. {held -> HoldForm, from -> z} /. (z[y__] :> Block[{},
     to @@ MapThread[#1 @@ (#2 /@ #3) &,
       {{from, Sequence}, {f, #&}, GatherBy[{y}, Head]}] /; True]) //. HoldForm->held];

{code1, code2} = {11, 22};
testa := (Print@"A"; 2);
testb := (Print@"B"; 4);

rep[expr, ToString, StringJoin -> StringForm, #<>"``" &] // InputForm
Hold[{code1, StringForm["asdad `` adsd ``",
     ToString[testa], ToString[testb]], code2}]

It is general enough to deal with other types of replacements:

expr = Hold[{code1, 1 + N@testa + 3 + N@testb, code2}];
rep[expr, N, Plus -> g, f] // InputForm
Hold[{code1, g[f[1] + f[3], N[testa], N[testb]], code2}]
$\endgroup$
4
  • $\begingroup$ @Mr.Wizard, Kuba Please see new version. $\endgroup$ Jan 17, 2014 at 15:11
  • $\begingroup$ The new version does not prevent evaluation of testa and testb, but neither does Kuba's original. My rule code seems a good deal simpler in this case. Also your code has ToString[testa] rather than bare testa, which does not behave the same; consider for example if testa is a "2D" expression. This could be an advantage or a disadvantage depending on what Kuba wants. $\endgroup$
    – Mr.Wizard
    Jan 17, 2014 at 15:25
  • $\begingroup$ @Mr.Wizard Yeah, I've realized that too. For the moment, I don't have time to figure out something clever. If I cannot do better tomorrow, I'll delete it. $\endgroup$ Jan 17, 2014 at 16:19
  • $\begingroup$ @Mr.Wizard Figured out something. $\endgroup$ Jan 17, 2014 at 16:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.