Jump to content
AHunter3

Need "Recursive Custom Functions for the Complete Idiot", please...

Recommended Posts

AHunter3

In a broad general sense I grasp the power of being able to have a recursive custom function. As Scott Love, et. al. write in Using FileMaker 8, which I just borrowed, put it, "As an abstraction tool, custom functions can alwasy be replaced in a formula by the logic they've abstracted. No such substitution can be made when dealing with recursive functions. In those cases, using custom functions is not a convenience; it's a necessity."

 

Or, to put it another way, you can write formulas in a custom function that you flat-out can't write in a calc fields' field definition. It obeys different rules, insofar as it lets you reference your own custom function within itself.

 

Therein lies both the wonderfulness and my current problem: maybe I'm brain dead but the simple examples that are supposed to explain to me how to generate the exit condition don't explain to me how to generate the exit condition.

 

I could ask this question two ways: "Here is specifically what I'm trying to create and I can't make it work"; or "Umm, OK, let's say hypothetically you had this string, and you want to output what lies between one delimiter and another where your delimiter's occurrence is the xth and you want to keep doing that and incrementing x until you've hit PatternCount (string, delimiter)...."

 

I myself have found it frustrating when other people try to pose their questions in vague general terms, so I'm going to do it the first way. But the purpose of this question is not to solve the problem but rather to get a handle on why the solution is in fact the solution so that next time I can write one myself, OK?

 

Here's the deal: I want, as my first excursion into writing recursive custom functions, to write one that takes a folder path submitted in Unix terms, for examples:

 

/Volumes/Images/Logos/Current

 

/Users/AHunter3/Documents/Work in Progress/

 

 

 

and return as output the same folder paths expressed in AppleTalk-ese:

 

folder "Current" of folder "Logos" of volume "Images"

 

folder "Work in Progress" of folder "Documents" of folder "AHunter3" of folder "Users" of startup disk

 

 

Keep in mind that the ability to get that output IS NOT WHAT I WANT. (As you will see shortly, I have a working method). What I want is to be able to write a custom function that will GIVE me that output, with the emphasis totally on "I want to be able to write a custom function".

 

I chose this task because it is intrinsically recursive. I currently do it via script — after adding a "/" if one is missing from either end of global field g.FileList, and snipping off the /Volumes/VolumeName part of the Unix path and dumping VolumeName into a variable $Volume if the path isn't to the startup disk, I do this loop in the script:

 

Set Variable [$Pos, PatternCount ($UnixPath, "/")

Loop

..Set Variable [$ASFolderPath, Let ($Qmk = "\""; $ASFolderPath & " of folder " & $Qmk & Middle ($UnixPath, Position($UnixPath, "/", 1, $Pos-1), Position($UnixPath, "/", 1, $Pos) - Position ($UnixPath, "/", 1, $Pos-1)-1) & $Qmk)]

..Set Variable [$Pos, $Pos -1]

Exit Loop If [$Pos =1]

End Loop

 

Post-loop, I do this:

 

Set Variable [$ASFolderPath, Let ($Qmk = "\""; Case (PatternCount(MyTable::UnixFolderPath, "/Volumes/")=0, $ASFolderPath & " of startup disk"; $ASFolderPath & "of disk " & $Qmk & $Volume & $Qmk) )]

 

 

So here's my first attempt at doing this recursive operation as a custom function (and please don't laugh at the silly way I nest Let() functions instead of listing them properly):

 

ASFolderPath [unixPath] =

 

Let (UnixP1 = Case(Middle(UnixPath; 1; 1)≠"/"; "/")&UnixPath & Case(Middle(UnixPath; Length(UnixPath); 1)≠"/"; "/");

Let ( UnixP2 = Case(PatternCount(UnixPath; "/Volumes/")=0; UnixP1; Middle(UnixP1; Position(UnixP1; "/"; 1; 3); Length(UnixP1)));

Let ( Pos = PatternCount (UnixP2; "/");

Let (Vol = Case(PatternCount(UnixPath; "/Volumes/")>0; Middle (UnixP1; Position(UnixP1; "/"; 1; 2)+1; Position(UnixP1; "/"; 1; 3) - Position (UnixP1; "/"; 1; 2)-1));

Let (Qmk = "\"";

 

 

Case(Pos >1, ASFolderPath & " of folder "& Qmk & Middle (UnixP2, Position (UnixP2, "/", 1, Pos-1)+1, Position (UnixP2, "/", 1, Pos) - Position (UnixP2, "/", 1, Pos-1)-1 & Qmk)

& Case (isempty (Vol), " of startup disk", " of disk "& Qmk &Vol & Qmk)

 

 

)))))

 

 

Now, what happens when I attempt to hit the "OK" button is that it highlights where I've tried to incorporate ASFolderPath into its own definition. Hmm, well of course it does, I haven't provided it with any UnixPath to work from. What I want it to reference at that point is "what you've got so far".

 

Then there's the problem of how do I tell the custom function to keep thinking of Pos as itself minus 1 until it grounds out at 1, my "exit condition"?

 

I don't understand how that works in the supposedly simple examples like this one in the book:

 

 

RepeatText [text; numberOfRepetitions] =

 

text & Case (numberOfRepetitions > 1; RepeatText (text; numberOfRepetitions - 1))

 

 

I'm staring at that and I'm not seeing what causes FileMaker to say to itself "Oh, and now I should reset numberOfRepetitions to itself minus 1". For that you can point fingers and laugh. I'm feeling pretty stoopid, so you probably won't make it any worse.

 

OK, that's it in a nutshell.

 

I want to reiterate one more time that the goal here is to understand how to do custom functions that are recursive, which reference themselves within their own formula until some exit condition is met. No matter how well you may know of a better solution to abstracting an AppleScript-ese filepath from a unix file path, or a way to use a Unix file path in AppleScript to begin with, etc, please keep in mind that right now I don't care about that, OK? Thanks.

Share this post


Link to post
Share on other sites
comment

Instead of breaking my head over your example, let's take the simple RepeatText() function. Suppose the function was called like this:

 

RepeatText ("abc" ; 3 )

 

Since the function is:

 

text & Case (numberOfRepetitions > 1; RepeatText (text; numberOfRepetitions - 1))

 

the result of the first iteration is:

 

"abc" & RepeatText ( "abc" ; 2 )

 

The second part of the result is an expression that requires further evaluation. After this happens, we get:

 

"abc" & "abc" & RepeatText ( "abc" ; 1 )

 

Again, the last part is calling a function, so this too needs to be evaluated. But this time, the Case() test will return FALSE. So the result is:

 

"abc" & "abc" & "abc"

Share this post


Link to post
Share on other sites
Ender

A good way to do recursive CFs that work on text is to take the whole text string and chop off the first bit, do something with that, then send the rest of the text string to another call of the CF. This continues until the input string is empty.

 

So with this in mind, build your recursive CF to first look for the stopping condition (the empty string), then the part that deals with a particular slice of the string, then add the recursive call to handle the rest.

 

In your specific example, you will need to treat the Volume part of the path differently than the rest, so you might use a separate calling CF that handles that, then calls the recursive part that deals with the rest.

 

For readability, I'd suggest using the bracketed form of the Let() function if you wish to define multiple variables.

Share this post


Link to post
Share on other sites
cando
OK, that's it in a nutshell.

I'd hate to see the long version. smiley-wink

 

But I certainly empathize with the problem of developing a process for creating recursive functions. You can test a script with breakpoints or build up a calculation bit by bit to see if you're heading in the right direction, but building a recursive function often becomes like programming by guess and prayer: How do you test it without testing the whole darn thing? I think Ender's idea goes part of the way, but not all recursive functions rely on the same kind of exit condition.

Share this post


Link to post
Share on other sites
AHunter3

comment, thank you for helping me with this.

 

the result of the first iteration is:

 

"abc" & RepeatText ( "abc" ; 2 )

 

The second part of the result is an expression that requires further evaluation. After this happens, we get:

 

"abc" & "abc" & RepeatText ( "abc" ; 1 )

 

Right there I'm lost. Why isn't it:

 

"abc" & "abc" & RepeatText ("abc"; 2)

 

 

I accept that it's not, I just don't understand why it's not and therefore don't know how to intentionally make it do that in some other context. The value — actually 1 but where I'd expect 2 — is what gets plugged in as the outcome of numberOfRepetitions - 1. What, how and where in the equation is something-or-other that sets numberOfRepetitions to itself minus one? That's where I'm feeling blind and stupid.

 

Again, the last part is calling a function, so this too needs to be evaluated. But this time, the Case() test will return FALSE. So the result is:

 

"abc" & "abc" & "abc"

 

Since I don't see what changes the value of the equation

numberOfRepetitions - 1 from 2 to a lower number each iteration, I don't see why you don't end up with an infinite "abc" factory.

Share this post


Link to post
Share on other sites
Ender
Since I don't see what changes the value of the equation

numberOfRepetitions - 1 from 2 to a lower number each iteration, I don't see why you don't end up with an infinite "abc" factory.

In comment's example, "numberOfRepetitions - 1" is evaluated before being passed to a new instance of the CF. So with each interation, the parameter is reduced by 1.

 

Think of each iteration of a recursion as being a new instance, having it's own inputs and it's own variables. With recursive functions, the results of an instance depend on the result of all the recursive children being completed and passed back. They all kind of go on hold waiting for the last child to finally pass its result back to its parent, and those results being passed up the chain.

Share this post


Link to post
Share on other sites
AHunter3

OK, that's what wasn't self-explanatory to me. The value of any variable used in the equation is evaluated from the inside out?

 

So an even simpler

 

CustomFunction [x] =

 

 

x & Case (x > 0, " " & x - 1)

 

 

would evaluate to

 

3 2 1

 

if I run CustomFunction (3) -- ?

Share this post


Link to post
Share on other sites
AHunter3

Nope. CustomFunction (3) yields "3 2". Worse, CustomFunction (19) yields "19 18".

 

well duh, I'm not referencing CustomFunction within itself, am I? Hang on.

 

I am intelligent, I am intelligent...

Share this post


Link to post
Share on other sites
AHunter3

Yay, my first (albeit ridiculously simple and albeit screwed-up the first time) recursive custom function!

 

 

x & Case( x > 1; " " & CustomFunction(x - 1))

 

 

CustomFunction (19) yields

 

19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1

Share this post


Link to post
Share on other sites
Ender

Yay!

 

Now the same idea can be used to parse a text string. In your AppleScript example, you would parse from the end to the front, using the "/" as the separator. Instead of sending x-1 as the reduced parameter, reduce the parameter string instead.

Share this post


Link to post
Share on other sites
AHunter3

Yep. Where I'm stuck at the moment is that in order for it to be recursive, I need ASFolderPath to call itself, and within the confines of that occurrrence of self-within-self, to ratchet down the position of the "/". But the position is not itself readily abstractable from anything I know how to point to, i.e., I'm trying to figure out how to say Case (Pos-1 >1, ASFolderPath (UnixFolderPath "except-with-the-Let(Pos=PatternCount (UnixP2, "/"))-decemented-down-one-if-you-please) in language that it can understand.

Share this post


Link to post
Share on other sites
Ender

Break the problem down. If you know how many slashes there are, you can find the position of the last slash, and from that the last folder name and the string up to that point.

Share this post


Link to post
Share on other sites
AHunter3

If Pos is established within the CF def as PatternCount (UnixFilePath, "/"), it's going to get re-established as that every time ASFilePath gets called recursively, isn't it? So that would make it convenient to be able to set it to that patterncount value only if it is currently not set to anything. Hmm...

Share this post


Link to post
Share on other sites
AHunter3

OK, I'm finally getting output. It ain't right but it isn't a beachball. Time to see if I can smack it into shape... thanks again, comment, Ender... I will report back later :)

Share this post


Link to post
Share on other sites
comment

You can do this in EXACTLY the same way as your countdown function:

 

BreakPath ( path ) =

 

topmostFolder & Case ( countFolders > 1 ; BreakPath ( remainingFolders )

 

where:

topmostFolder is derived by Left() to the first "/";

countFolders is derived by PatternCount ( path ; "/" ); and

remainingFolders is found by Right() from the first "/".

 

(roughly)

Share this post


Link to post
Share on other sites
Ender
If Pos is established within the CF def as PatternCount (UnixFilePath, "/"), it's going to get re-established as that every time ASFilePath gets called recursively, isn't it? So that would make it convenient to be able to set it to that patterncount value only if it is currently not set to anything.

 

Remember: the incoming parameters and variables are only relevant to the current instance of the CF. If the number of slashes is useful to know at each instance (which I think it is since it changes as the string reduces), then define it as a variable within the recursive CF. Same with the position of the last slash, the value of the last folder name, and the remaining string.

Share this post


Link to post
Share on other sites
AHunter3

:D Got it!

 

Used two custom functions, one nested within the other.

 

ASFolderPath [unixFolderPath] =

 

Let ( [unixP1 = Case(Middle(UnixFolderPath; 1; 1)?"/"; "/")&UnixFolderPath & Case(Middle(UnixFolderPath; Length(UnixFolderPath); 1)?"/"; "/");

UnixP2 = Case(PatternCount(UnixFolderPath; "/Volumes/")=0; UnixP1; Middle(UnixP1; Position(UnixP1; "/"; 1; 3); Length(UnixP1)));

Vol = Case(PatternCount(UnixFolderPath; "/Volumes/")>0; Middle (UnixP1; Position(UnixP1; "/"; 1; 2)+1; Position(UnixP1; "/"; 1; 3) - Position (UnixP1; "/"; 1; 2)-1));

Qmk = "\""];

 

ASFPCoreFx ( UnixP2 ) & Case (IsEmpty (Vol); " of startup disk"; " of disk "& Qmk &Vol & Qmk)

 

)

 

...and the function that THAT function calls...

 

 

ASFPCoreFx [unixP2] =

 

Let ( [ Qmk = "\"";

$Pos = Case(not IsEmpty ($Pos); $Pos - 1; PatternCount (UnixP2; "/"))

];

 

" of folder "& Qmk & Middle (UnixP2; Position (UnixP2; "/"; 1; $Pos-1)+1; Position (UnixP2; "/"; 1; $Pos) - Position (UnixP2; "/"; 1; $Pos-1)-1) & Qmk

 

& Case ($Pos-1 > 1; ASFPCoreFx ( UnixP2 ) )

 

 

)

Share this post


Link to post
Share on other sites
Ender

I haven't analyzed your algorithm for efficiency, but I have a couple suggestions for readability:

1. Format your CF name/parameters with parenthases instead of brackets.

2. Use short but descriptive names for variables.

3. Use "=", and "" for inequalities since they work fine in FileMaker and transfer well to web pages.

4. Use spaces to add indents to the CF for each level. A handy tool for this is:

http://www.aptworks.com/tools/index.html

5. Comment! That's right, dig out the old double slash and explain what the function does, and if it's not obvious, what the variables are.

6. Give parameters and variables names that begin with a lowercase letter. Use the FileMaker function parameters as a guide.

 

One operational thing I noticed with your $Pos variable: I'd strongly suggest avoiding the use of global variables, as the values could persist and cause trouble later with the same or another CF. Instead compute the values in each instance, or if a value must be propogated from a previous instance, send the value as another parameter.

Share this post


Link to post
Share on other sites
AHunter3
One operational thing I noticed with your $Pos variable: I'd strongly suggest avoiding the use of global variables, as the values could persist and cause trouble later with the same or another CF. Instead compute the values in each instance, or if a value must be propogated from a previous instance, send the value as another parameter.

 

I was going to ask if that was a bad idea. I resorted to it because I needed to set it to itself minus one if it was not empty, and the formula-gremlin would not let me set Pos to Pos-1 if Pos was not empty, whereas I could set $Pos to $Pos-1 without any complaints.

 

Quite aside from "yeah but it works", insofar as the original intention was to learn how to do this and to do it with a decent understanding rather than just muddling through, I'm game for looking for ways to avoid the dollar sign strategy. Just because i'm only likely to invoke this function in the course of a script doesn't mean that next time I want to do this I won't be attempting to incorporate the function into a field def!

 

So how does one say Let (Pos = Case not IsEmpty (Pos), Pos-1, ElseUseFormula _to_createPos) ? This very clause is the first occurrence of Pos (or $Pos).

Share this post


Link to post
Share on other sites
Ender

Hunter, your ASFPCoreFx function seems to be traversing the entire string each time instead of reducing the string. This is essentially an iterative approach as the the number of iterations is known when the function is first called. The recursive algorithm reduces the remaining string with each recursion, and is how I would handle this problem.

 

If you wish to go with your iterative approach, send the index (your Pos variable) as a parameter so it knows when the stopping condition is met. The initial call from the parent function can send the patterncount().

 

Since this is an exercise for you, it may be beneficial to try both the iterative and the recursive algorithm on this.

Share this post


Link to post
Share on other sites
AHunter3

comment and Ender, thank you again, I'm not ignoring your latest advice and recommendations, I'm trying to parse them ;)

 

Seriously, there is a bit of a learning curve on this thing for me. Where custom functions just subsume a messy box of calcs that you could otherwise enter in a field def or the right-hand side of a Set Field script step, it's effortless, but there are a different set of rules in play here and it's going to take me awhile to really get a feel for what I can do and how best to do it.

 

(Are there other things aside from recursion that you can do in custom fx that you can't do in a calc field, etc, btw?)

 

Appreciate your tips.

Share this post


Link to post
Share on other sites
Ender
(Are there other things aside from recursion that you can do in custom fx that you can't do in a calc field, etc, btw?)

Recursion is the thing that regular calcs can't do, but CFs are also useful for hiding the complexity of a complex calc, especially when the guts of the calc might be needed in multiple fields. You could also use CFs for defining constants, like tax rates or something.

Share this post


Link to post
Share on other sites
AHunter3

Aye, have done so, and that part is all self-explanatory. I have one that replaces Middle for snipping values between delimiters in a longer string (although I've done Middle for so many years I can do it in my sleep, it's still easier to use the Custom fx version just to eliminate the bounty-crop of parentheses!), for instance.

 

So recursion is the only totally-new thing I need to worry about not having a grasp of yet. It's a wonderful feature but the part of me that really hates being left in the dust by changes I don't understand yet is kind of relieved to hear that it's the only one of that nature in custom functions.

 

(I've only had a copy of Advanced since February, did everything in plain-old FmPro 8.5 up until then)

Share this post


Link to post
Share on other sites
This thread is quite old. Please start a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.




×
×
  • Create New...

Important Information

Terms of Use