#2902 How to declare a 'constant' variable?

Gary Mon 1 Jan

I scoured through the documentation but I could not find a solid example of declaring a constant in Fantom. I see the keyword const is used but it appears to be only usable within a class field. Here's a simple IO console app written in Java that I'm trying to translate:

package com.mycompany.hello_world;

import java.util.Scanner;

public class Hello_world {

    public static void main(String[] args) throws InterruptedException {
        var in = new Scanner(System.in);
        
        final String Q = "Q";
        String input;

        do {
            System.out.print("> ");
            input = in.nextLine();
            if (input.isEmpty()) {}
            else System.out.printf("> You wrote '%s'\n", input);         
        } while(! input.equalsIgnoreCase(Q));
        
        System.out.println("> Goodbye!");
        Thread.sleep(2000);
    } 
}

Here's my attempt in Fantom. I couldn't simply write const String Q = "Q" within main. So I enclosed it in a class and it works, but am I missing something?

using concurrent 

class Thing { 
  static const Str Q := "Q"  
}

class Main
{
  static Void main() {
    // const Str Q := "Q"
    Str? userInput := "" 

    echo("Press <q> to exit..") 
    while (userInput?.upper != Thing.Q) {
      userInput = Env.cur.prompt("> ") 
      if (userInput.isEmpty) continue 
      echo("You wrote '$userInput'")
    } 
    echo("Goodbye!")
    Actor.sleep(2sec)
  }
}

Thanking everyone in advanced! And Happy New Year! :D

Henry Mon 1 Jan

Hi chikega,

I may be speaking out of turn here, so you may also want to wait for some confirmation from one of the more experienced Fantom programmers.

As I understand it, the notion of a const variable doesn't make much sense, since const guarantees no state changes, a variable wouldn't be useful. As such I believe the compiler won't recognise const terms being used inside methods, probably resulting in an Expected expression, not const compilation error?

You can define const Fields within classes, as you have done for your Thing class, and you can define classes themselves as const (see Const classes in the Classes docs)

With how you appear to be using it though, I would recommend just using a static const field on the main class itself to denote your key string for quitting the prompt, something like:

using concurrent::Actor

class PromptClass {
	
	static const Str exitKey := "Q"
	
	Void main() {
		userInput := ""
		
		while (userInput.trimToNull?.lower != exitKey.lower) {
			if (userInput.trimToNull != null)
				printLine("You wrote '${userInput}'")
			userInput = env.prompt("> ")
		}
		printLine("Goodbye!")
		Actor.sleep(2sec)
	}
	
	private Void printLine(Str s) {
		env.out.printLine(s)
	}
	
	private Env env() {
		Env.cur
	}
	
}

I've also made a couple of syntax changes to give you some insight on how I'd go about putting together a command line project as you seem to be doing. Let me know if you want me to explain any of the changes, and feel free to ignore them if you wish!

brian Mon 1 Jan

Fantom does not support the notion of const local variables, const is only used at the class and field level. However, there is the notion of effectively final which is used to determine if a local variable may be captured as by an immutable closure.

Gary Fri 5 Jan

Thank you Brian and THANK YOU Henry for refactoring my code. I'm not sure if I fully understand it all. But, the original Java code is based on a video tutorial by Huw Collingbourne on creating a text-based adventure game. Huw wrote an early commercial text-based game back in the 80's using Pascal. Henry, I've been perusing your refactored code for the last couple days in attempt to digest what I'm looking at. I even had ChatGPT explain to me some of the syntax and design choices(i.e. helper classes). But I would love to hear your thoughts on your design choices in this refactored code. It looks like pro-level stuff to me!

Henry Sat 6 Jan

For the helper methods, the rationale is just generally that typing out Env.cur.out.printLine or Env.cur.prompt more than a few times in a project takes a while, and we know that both Env.cur and Env.cur.out.printLine are going to be used often within any kind of console-based project. Essentially just to save the effort of typing out the whole thing every time, instead now just being able to use env.<call> and printLine(<msg>) instead.

For the changes to the while loop, though I believe Fantom has no isses with the Java syntax of continue / break etc, my personal preference is to remain within the explicit Fantom syntax, though this does require some re-ordering of the operations of the while loop and won't work for all situations, logically (in this case, I print whatever has been typed first, then prompt for a new entry - in order to avoid the "You wrote <x>" message from appearing prior to the "quit" action being performed (granted it probably doesn't matter THAT much to most people but I'm a little particular)

I think the only other difference is the string comparison in the while loop condition, in my case I used lower on both sides of the conditional, since it didn't seem important for it to be case sensitive. (calling upper on both works just the same, can't give you a good justification why my brain goes to lower first!)

Logically, since the key word being used to map to "quitting" the loop is just a static field, there's no necessity to convert both sides of the conditional to either upper or lower case, as you would hopefully remember to only ever set it to a fully uppercase string. However since we are all still human, and this seems like a common place for a mistake to occur, I convert both sides to be safe, for example if you were to change the static exitKey from "Q" to "esc", with the original code, it would be impossible to quit the program using the prompt (since the userInput?.upper could never result in a case matched "esc", and thus would never exit)

Additionally, things like mapped keybinds or key-prases such as this are likely to be the kind of thing that eventually gets pulled out to some kind of configuration, which has a high chance of being edited by someone who may not be intricately aware of how the code itself works, and so thus it would hopefully protect in some respect the possiblity of a mistake there!

Gary Sun 7 Jan

Wow! Thank you so much for taking time out to share your thought process on this bit of code. It means a lot to me to see through the eyes of a professional developer. I'll be reading it more than a few times as I digest it all, combining your thoughts with ChatGPT and Claude2's insight. I was surprised how much generative AI knows about Fantom syntax and constructs! In my Java code, I've even shifted the final String Q = "Q"; outside of Main and made it static to more closely match the Fantom version. I normally go to lower case myself. But when I made the variable constant, I capitalized it out of habit from my C coding habit and I felt compelled to make the string literal upper case as well. But as you mentioned, I guess it doesn't really matter. Nevertheless, I like your what you did much better.

I do have one little question. When I code in C#, intellisense will usually suggest that I make the userInput variable nullable (eg. string? userInput). That's why I coded it similarly in Fantom (eg. Str? userInput := ""), but I noticed that you didn't. If I changed my original code to Str userInput := "", I will get the error message: Cannot use null-safe call on non-nullable type sys::Str' directed to while (userInput?.upper != Thing.Q). It looks like your safe call operator is affecting trimToNull and is not directed at your non-nullable userInput. It appears that I'm just writing my thoughts down in real time as I think about the refactored code. :) In any case, what are your thoughts on making userInput non-nullable?

Henry Sun 7 Jan

I believe I did technically miss something there.

Since we instantiate userInput with an empty string, it's type is implicitly set as a non-nullable Str, and as far as I was aware, Env.prompt wouldn't ever return null, so there was never a risk of userInput being set to null at any point, that was my rationale behind making it non-nullable.

However, upon reviewing the documentation, it seems in fact that Env.prompt has a return type of Str?, and according to the docs - Return null if end of stream has been reached.. I'm not sure how that would occur without exiting the current thread and thus it not mattering, possibly it's something that can happen with the JLine integration of which I'm not familiar, one of the more experienced Fantom users might be able to explain that for you.

So userInput probably ought to be a nullable string, and you'd then need to update the calls to trimToNull to also be null-safe, so:

while (userInput?.trimToNull?.lower != exitKey.lower) {
	if (userInput?.trimToNull != null)
		printLine("You wrote '${userInput}'")
	userInput = env.prompt("> ")
}

Either that, or you can keep userInput as non-nullable add an elvis after the call to prompt to make sure userInput is never set to null

while (userInput.trimToNull?.lower != exitKey.lower) {
	if (userInput.trimToNull != null)
		printLine("You wrote '${userInput}'")
	userInput = env.prompt("> ") ?: ""
}

This way even if the Env.prompt somehow returns us null, we intercept it before it's set on userInput and replace it with the empty string.

The trimToNull there is just a general catch-all for if people respond to the prompt with whitespace characters, since there's not much point in printing the response if it's just whitespace! It shouldn't actually affect the logic for continuing/breaking the while loop.

Gary Sat 20 Jan

Thank you Henry again for your adept insights and allowing me to "peer over your shoulder", so to speak. :)

Login or Signup to reply.