Notifications
Clear all

[Closed] Packaging/defining functions in a Struct?

 am7

As far as I’ve understood from reading posts in this forum, it’s best to package your functions in a Struct and also define them at the beginning to avoid keeping alot of attributes in the global scope. Please correct me if I’m wrong about that.

Now to my question, I’m wondering if I have to supply parameters in the function definitions at the top of the Struct. The following code work fine, so I’m guessing that the function definition of fooMe is overrided as it’s being defined the second time?

Is this ok to do, or is it best practice to include the parameters in the initial definition of the functions?


( -- go into local scope
	struct Test -- define Struct for packaging functions
	(
		-- initial definitions of the functions
		fn testMe = (), fn fooMe var = (),
		
		fn testMe = (
			format "testMe() called
"
		),
		
		-- override initial definition and supply a parameter to the function definition
		fn fooMe var = (
			format "fooMe() called, and var contains = '%'
" var
		)
	)
	
	-- test it to see if it works
	MyTest = Test()
	MyTest.testMe()
	MyTest.fooMe "FOO!"
) -- end local scope

11 Replies
1 Reply
(@focomoso)
Joined: 11 months ago

Posts: 0

Just to clarify, fooMe is not overridden in the traditional oop sense. It is simply redefined. Overriding requires inheritance and both definitions of the function remain valid depending on whether you’re working on the parent or child.

Also, max does not support polymorphism so this:


struct Test -- define Struct for packaging functions
(
	-- initial definitions of the functions
	fn fooMe var = (),
		
	fn fooMe = (
		format "fooMe() called no params
"
	),

	-- override initial definition and supply a parameter to the function definition
	fn fooMe var = (
		format "fooMe() called, and var contains = '%'
" var
	)
)

-- test it to see if it works
MyTest = Test()
MyTest.fooMe() -- error here!
MyTest.fooMe "FOO!"

throws an error because fooMe, while first defined as taking no parameters, was redefined to require one. In true oop, both definitions would be valid and the compiler would call the proper one based on the number of parameters supplied.

And you are right that it’s good practice to define your functions at the top for the reasons you mention. You can then organize your code as you like rather than having to be sure certain definitions come before others (and it is possible to have a situation where you have to pre-define because of a conditional circular dependency). In a small macroScript I don’t do it, but anything larger I try to keep up the habit.

Hi Atle,
it is safe to say the least in global scope, the best. Anyway, before packing your functions into a Struct, you should consider that functions and variables defined into a Rollout are local to it, the same goes for functions and variables inside a MacroScript. You can use the “local” keyword to make it clearly visible but is not required. In my experience Structs are useful when you need to store and manipulate organized data chunks and make some repetitive operations on them, like initializing some data, then run struct functions to calculate some other data from first set.

About the function definitions into a Struct, there isn’t such a thing like functions declaration in MaxScript, neither forward declaration, like in C++. The only thing you need to do is to define the function you need before calling it: name it, set arguments, write the body, that’s it. You can safely take away the double definition. Once the Struct is instanced, you can call every function inside it, as well as passing parameters to their arguments.

Following code works fine:

( -- go into local scope
    struct Test -- define Struct for packaging functions
    (
        /* this function definition is unneeded
        -- initial definitions of the functions
        fn testMe = (), fn fooMe var = (),
        */

        /* by keeping first function definitions, these would be redefinitions not overrides */
        fn testMe = (
            format "testMe() called
"
        ),

        -- override initial definition and supply a parameter to the function definition
        fn fooMe var = (
            format "fooMe() called, and var contains = '%'
" var
        )
    )

    -- test it to see if it works
    MyTest = Test()
    MyTest.testMe()
    MyTest.fooMe "FOO!"
) -- end local scope
  • Enrico
 am7

Thanks for the reply Enrico!

So if I prefix the attributes outside a struct with ‘local’ like you mentioned I could remove the initial “brackets” and stay in global scope?

As for the initial function declerations… I read on this forum that was good practice to define the functions at the top (even if it’s not needed) to avoid the “Call needs function or class, got: undefined” errors.

From what I can understand this error seems to relate to the fact that 3ds max evaluates the script before running it – and sets the function to undefined if it’s called before it’s declared.

ie:


  (
  	function test = (
  		format "test
"
  		test2()
  	)
  	
  	function test2 = (
  		format "test2
"
  	)
  	
  	test()
  )
  

Will throw an error: Type error: Call needs function or class, got: undefined

Adding the declerations to the top will fix this error:


   (
 	local test
 	local test2
 	
 	function test = (
 		format "test
"
 		test2()
 	)
 	
 	function test2 = (
 		format "test2
"
 	)
 	
 	test()
 )
   

This behaviour is different from other programming languages, and it might be hard to keep track of all the functions and function calls. So to avoid functions being set to undefined, it was suggested to add empty function declerations at the top as a best practice.

Brackets define a block of code, that’s like a scope level, so from global you go into first scope level and run the code inside it. Everything inside the block is executed and if variables and functions are defined inside it, they’re destroyed at the end of the scope without leaving anything in global scope, unless you explicitly define it as global, like: global var = “myGlobal”

Local keyword cannot substitute a block of code and its scope, as a matter of fact it is forbidden in global scope. It is like a visual remainder for the standard behaviour: in example inside a rollout writing: local var = “myLocal”, is the same as var = “myLocal”

I cannot tell if callback functions are allowed in structs, never had the occasion to try it.

Well, I learn in this moment MaxScript allows something like C++ forward declaration, which means define a function, call it, then define it. Anyway I won’t enforce it as a coding standard, but introduce it only where it is absolutely needed. Your sample code could invert the functions declaration order to work well.

I prefer:

(
    function test2 = (
        format "test2
"
    )

    function test = (
        format "test
"
        test2()
    )

    test()
)

rather than:

(
    function test2 = ()
    -- or local test2 which looks like (and is) a variable declaration

    function test = (
        format "test
"
        test2()
    )

    function test2 = (
        format "test2
"
    )

    test()
)

Structs in MaxScript aren’t as powerful as objects in OO languages, so I doubt you’ll end up creating such complex structures hard to manage. If you write MacroScripts, as it is most of the times, you won’t need to pack functions in structs, as they will stay local to the script. You’ll find quite annoying having to call struct.method every time you need a function. Another way to keep your code easy to read and maintain, as well as build function libraries, is to split your code in different files then use “include” or “fileIn”. You won’t need to call the struct and it can keep functions inside local scope.

At the end of the day what matters is to make things work, if you find comfortable to predefine functions, do it, it won’t hurt, but you’ll need to keep updated two definitions.

  • Enrico
 am7

Thanks Enrico for clearing that up!

So I’ll drop the struct packaging the functions then, as I don’t need them to be global (the idea of struct as I understood it is to package global functions and attributes, to help avoid conflicts with other scripts).

On a sidenote: I did some testing on the callback functions, and it seems as a callback function can be put into a struct. But both the struct and the instance of the struct needs to be global in order to call it from callbacks.addScript.

I guess I’ll just put the callback function outside my scope level, and keep the rest of the functions inside the scope level.

 am7

James: thanks for clearing that up, I have an OOP programming background so MaxScript can be confusing at times.

If I need to call another function from previously declared function I prefer to define second function as local variable before declaration of first one:


(
local fun2
fn fun1 = fun2()
fn fun2 = print "f2"
 
fun1()
)

Yes, I can just declare fun2 first:


(
fn fun2 = print "f2"
fn fun1 = fun2()
 
fun1()
)

But what I have to do if need to call fun2 from fun1, and call fun1 from fun2 (sounds stupid? not exactly). Here is only one way – to define them both as local variables before declare them as functions:


(
  local true_act, false_act
  fn true_act act list:#() = 
  (
	if act == true then append list 1 else if act == false do list = false_act act list:list 
	list
  )
  fn false_act act list:#() = 
  (
	if act == false then append list 0 else if act == true do list = true_act act list:list 
 list
  )
  fn true_false acts:#() = 
  (
	list = #()
	for act in acts do list = true_act act list:list
	list
  )
  true_false acts:#(on, on, 0.0, off, on, undefined, off)
)


4 Replies
(@focomoso)
Joined: 11 months ago

Posts: 0

Exactly: conditional circular dependency.

(@denist)
Joined: 11 months ago

Posts: 0

I agree that it’s not a good way of programming, but my sample doesn’t have circular dependency… and doesn’t do anything illegal… and works.

(@focomoso)
Joined: 11 months ago

Posts: 0

I think you misunderstood me. I was agreeing with you. It’s a legit case and a reason why you might have to declare your functions first.

(@denist)
Joined: 11 months ago

Posts: 0

Sorry. I’m really misunderstood you. Technically circular dependency is an anti-pattern thing. It might cause serious problems. But unfortunately sometimes it’s only one way to solve a task, specially if some modules were designed and developed by different people.
But honestly I always avoid using it in MAX scripting. The sample shown by me above is just an abstract sample.