Forums

Prompt class (please give your opinion)

General discussion about Cobra. Releases and general news will also be posted here.
Feel free to ask questions or just say "Hello".

Prompt class (please give your opinion)

Postby jonathandavid » Sat Jan 17, 2009 5:49 am

Following the discussion here, I've taken Chuck's challenge and decided to write a class for performing formatted input in Cobra (the idea
is to have a Cobra equivalent of C's scanf of C++'s cin. I know there's already Console.readLine in C#, and more advanced options like BinaryReader, but we need something that is more expressive, extensible, and backend independent.

My solution is in the attached file.

The biggest part of the file is at the begining, inside Program.main. This is a very detailed explanation (a tutorial) of everything offered by the Prompt class, including how to extend it. You'll see that it's simple yet powerful, or that's what I've intended. I've also striven to make it robust, meaning that is hard to cause dangerous behavior in case the wrong input is entered. I've had to make a number of design decisions, some of which might be debatable or easy to improve. Please share any hints or suggestions that come to mind.

After the lengthy main function comes the code where Prompt is implemented, together with the auxiliary code needed to make main work (validators, formatters...) I've tried to make the implementation as simple as possible, although I admit things have gotten a little out of hand here and there. Feel free to offer any suggestions regarding the implementation as well.

I've written no tests, mainly because the class is so user-input oriented, and also because the main function covers most of the code pretty well. Feel free to add test code, it is fairly easy to automatize tests using the ability of Prompt to read from a TextReader that can be bound to a file or to a string.

BTW, kudos to Chuck for making Cobra such an enjoyable language to write code in. And this considering that I've written Prompt using no syntax hilighting or code completion!
Attachments
Prompt.cobra
A class implementing extensible and robust formatted input for Cobra
(24.8 KiB) Downloaded 159 times
Last edited by jonathandavid on Sat Jan 17, 2009 6:34 am, edited 1 time in total.
jonathandavid
 
Posts: 159

Re: Prompt class (please give your opinion)

Postby jonathandavid » Sat Jan 17, 2009 6:03 am

For those too lazy to download and open the attached file, here's what the tutorial looks like. It is quite lengthy, but I hope it's easy (and moderately fun) to follow.

_

# Basic usage. Read an int, with no prompt text, no error checking

i = Prompt().readInt
print 'you entered:', i


# A PromptException is thrown in case something goes wrong. The error cause can be queried
# Error causes:
# None. Nothing wrong happened.
# EOF. The EOF was found (typically when reading from a file/string)
# Cancel. The user-specified cancellation text was entered. By default, no
# cancellation text is used (i.e., there is no way to cancel)
# Validation. The user-specified validation predicated returned
# false. No validation is used by default
# Format. The user-specified formatter could not be applied. When a Prompt
# method of the readXxxx family is used (e.g., readInt), the
# formatter is internally set accordingly (e.g., intFormatter)

try
f = Prompt().readFloat
catch ex as PromptException
print 'something went wrong ([ex.errorCause])'
success
print 'successfully read [f]'


# If we plan to reuse the prompt on several operations, we can keep a reference to it
# This example also shows how easy it is to specify a prompt text. The 'text=' part can
# be omitted, we could have simply said Prompt('enter an int:')

simplePrompt = Prompt(text='enter an int:')
try
i = simplePrompt.readInt
catch
pass

# Note that all construction time arguments, such as 'text', can be changed after the
# prompt has been constructed

simplePrompt.text = 'enter an integer:'


# It is possible to query the error cause after the read operation

branch simplePrompt.errorCause
on Prompt.Error.None
print 'all right'
else
print 'ups, [simplePrompt.errorCause] error'

# The special property 'success' is equivalent to .errorCause == Prompt.Error.None
print if(simplePrompt.success, 'success', 'failure')

# The toString method of Prompt returns the user response, or the error cause in case
# an error occurred. If no input has been entered yet, it returns 'no input'

print simplePrompt # depends on what we entered in the last op.

print Prompt() # 'no input'


# It is possible to read from a given TextReader. For example, a StringReader

strReader = StringReader('3\n hello')

srcPrompt = Prompt(source=strReader)
i = srcPrompt.readInt
str = srcPrompt.readString
print i, str

# Note that we have to say srcPrompt.readString. There is a simpler version,
# srcPrompt.readString, but it returns an Object which must thus be cast
# accordingly. The above would have looked:
# str = srcPrompt.read to String
# Below, when we present how custom formatter are created, we'll see when
# it makes sense to call Prompt.read directy


# If we read further from the finished stream, we can test the EOF condition

try
srcPrompt.readFloat
catch PromptException
print srcPrompt.errorCause # 'EOF'



# Exceptions can be disabled from our prompt. But to do so, we must provide
# a default value that will be used in case of error.

otherPrompt = Prompt('enter an int (no excep.):', defaultValue=17)
i = otherPrompt.readInt
print i # 17 is something went wrong


# When defaultValue is specified, the prompt stops throwing in all subsequent
# operations. To bring it back to throwing, call the method restoreThrowing

otherPrompt.restoreThrowing
try
i = otherPrompt.readInt
catch PromptException
print 'Now it DID throw!'


# A cancellation text can be specified. The read operation fails (i.e., throws
# or returns the default value) when this text is entered

try
i = Prompt("enter an int ('c' to cancel):", cancelText='c').readInt
catch ex as PromptException
print ex.errorCause # Cancel is 'c' was entered, Format if some other non-int


# So far, we've covered the "no retry" operations mode of Prompt, in which
# the read operation is aborted as soon as something goes wrong. Promts also
# support a "retry on failure" mode of operation. The easiest way to enter this
# mode is with the 'retry' property. This causes the prompt to retry an endless
# number of times (or until EOF or the cancellation text are encountered)

retryPrompt = Prompt('enter a float (endless retry):', retry=true)
f = retryPrompt.readFloat # will loop until good input or exception


# A maximum number of retries can be specified (does not include the first one)
# Note that specifying retries > 0 eliminates the need for retry=true.
# Conversely, retries=0 is equivalent to retry=false
# The following will prompt the initial time and 3 additional times unless of
# course a valid input is entered

retryPrompt = Prompt('enter a float (3 retries):', retries=3u)
f = retryPrompt.readFloat

# We can go back to no retry afterwards
# retryPrompt.retry = false
# Or with:
# retryPrompt.retries = 0




# We can specify a text that is printed before every retry.
# Note that specifying non-nil retryText eliminates the need for retry=true.
# However, retryText=nil does not set retry=false. It simply removes the retry text
# As an extra goodie, the character # is replaced by the nb. of retries that are left
# (# can be escaped using ##)

retryPrompt = Prompt('enter a float (endless retries with msg): ',
retryText='Try again. You have # attempts left')
f = retryPrompt.readFloat

# Another example: 10 retries and retry text

n = 10u
retryPrompt = Prompt('enter a float ([n] retries with msg): ', retries=n,
retryText='Try again. You have #/[n] attempts left')
f = retryPrompt.readFloat



# Now we'll see how to create a custom response validator. A response validator
# takes the formatted response, and returns true if it's valid. For example,
# if the formatter is intFormatter, then the validator must work on ints.
# However, due to static typing restrictions, the validator receives the formatted
# response as an Object. The validator is only called if the formatting operation
# succeeded. Therefore, its argument is always a valid object of the formatted type
# (i.e., an int).
#
# Suppose we have the following validator for ints, defined as a shared method of
# a class MyValidators:
#
# def isEven(o as Object) as bool
# return (o to int) % 2 == 0
#
# Then, the following code uses this validator in order to forbid odd inputs
# (Note how I use defaultValue to achieve exception-less error checking)
# (Note, too, that in the current version of Cobra (0.8.0 post-release) we
# have to set the delegate property after construction, due to a bug that
# prevents using delegate properties directly in the constructor)


evenPrompt = Prompt('enter an even number:', defaultValue=2)
evenPrompt.validator = ref MyValidators.isEven
i = evenPrompt.readInt
branch evenPrompt.errorCause
on Prompt.Error.None
print 'an even number, [i], was entered'
on Prompt.Error.Validation
print 'Error: looks like you entered [evenPrompt.response], which is odd'
else
print 'Some other error occurred. Your input was [evenPrompt.rawResponse]'

# Two important additinal lessons can be learned from this example:
# (1) When a validation error occurs, prompt.response returns the formatted
# input that did not pass validation (it is an Object?, you might
# have to cast it, but make sure it is no nil). Note that when a validation
# error occurs, the result of p.readXxxx differs from p.response. The former
# is not set, or the defaultValue, whereas the latter is the is the formatted
# input that failed validation
# (2) When a general error occurs, so that not even the formatted input is
# available, prompt.rawResponse returns the text that the user entered
# (as String?)


# A similar example, with exceptions:
evenPrompt = Prompt('enter an even number (with excep.):')
evenPrompt.validator = ref MyValidators.isEven
try
i = evenPrompt.readInt
catch ex as PromptException
print ex


# We can change the validator later on, or eliminate it, by using the same property:

evenPrompt.validator = nil


# Another validator example, this time using a validator delegate that holds state
#
# class StringLengthValidator
# var _maxLength = 0
# def init(max as int)
# _maxLength = max
# def validate(o as Object) as bool
# return (o to String).length <= _maxLength

p = Prompt('enter a string, shorter than 10:')
p.validator = ref StringLengthValidator(10).validate
try
str = p.readString
catch Exception
print "no, that's not the idea"
success
print 'yeah, you got it right'




# Finally, we'll see how to implement a custom formatter. This allows us to extend
# the Prompt facility to our own classes. Consider the following formatter, defined
# inside a Rational class written by ourselves:
#
# def format(rawResponse as String) as Object? is shared
# res = Rational()
# if Rational.tryParse(rawResponse, out res) == true
# return res
# return nil
#
# It can be seen that by convention a formatter returns nil to signal that it could
# not perform the formatting. Otherwise, it returns the formatted object, converted
# to Object for static typing requirements
#
# This particular formatter relies on a Rational method (tryParse) to do the actual
# conversion from String (the raw user input) to Rational. This simple method simply
# num and den, separated by whitespace. The den part is optional
#
# This is how this formatter is used:

rationalPrompt = Prompt('enter a rational as "num \[den]":',
defaultValue=Rational())
rationalPrompt.formatter = ref Rational.format
r = rationalPrompt.read to Rational
if not rationalPrompt.success
print 'there was an error, using default value'
print r


# Note that we've had to use prompt.read, because obviously there is no predefined
# readXxxx method for Rationals. Thus the need to cast the result to Rational. This
# is tedious and error prone. Fortunately, extension methods offer an elegant
# solution. Inside the same file where the Rational class is defined, we can add
# the following extension to the Prompt class:

# A fancier way to achieve the same is to add extension methods to class Prompt
#
# extend Prompt
# def readRational as Rational
# .tempFormatterter = ref Rational.format
# return .read to Rational
#
# Once this method has been added to Prompt, we can simply write:

rationalPrompt = Prompt('enter a rational as "num \[den]":',
defaultValue=Rational())
r = rationalPrompt.readRational
if not rationalPrompt.success
print 'there was an error, using default value'
print r

# And voilá, our Rational class behaves, from the point of view of Prompt users,
# just like an ordinary Cobra primitive type

# One thing in the implementation of readRational requires explaining. Instead of
# using prompt.formatter, which changes the formatter until the end of times (or
# until a new one is specified), we've used prompt.tempFormatter. This way,
# the formatter being set we'll only remain until the next read operation.
# Afterwards, the previous formatted is automatically restored. Fancy isn't it?
# There are similar .tempXXX properties to temporary set the validator (.tempValidator)
# and the defaultValue (.tempDefaultValue)



# Note how easy it is to extend Prompt to read types with custom requirements:
#
# extend Prompt
# def readEvenInt as int
# .tempValidator = ref MyValidators.isEven
# return .readInt
#

try
i = Prompt('enter an even number:').readEvenInt
catch ex as PromptException
print 'an error occurred'
success
print 'succesfully read an even number ([i])'



# To conclude, please don't get carried away from the apparent complexity of some
# of the examples presented here. In its core, the functionality provided by
# Prompt is very simple:

i = Prompt('enter a number:').readInt
print i

f = Prompt('enter a float:', retryText='try again').readFloat
print f
jonathandavid
 
Posts: 159

Re: Prompt class (please give your opinion)

Postby Charles » Sat Jan 17, 2009 1:39 pm

jonathandavid wrote:BTW, kudos to Chuck for making Cobra such an enjoyable language to write code in. And this considering that I've written Prompt using no syntax hilighting or code completion!

Glad you enjoy Cobra. Just so people know, you can get syntax highlighting at the EditorSupport wiki page.

Also, making a test in Cobra is as easy as:
class Foo

test
f = Foo()
assert f.two == 2

def two as int
return 2

Well for that class, I would put the test with "two" but I wanted to show one at the class level because that's likely what you'll want with the prompt. And as you pointed out, you can use a StringReader.
Charles
 
Posts: 2510
Location: Los Angeles, CA

Re: Prompt class (please give your opinion)

Postby jonathandavid » Sat Jan 17, 2009 2:05 pm

I get the message, I'll write the tests when I have some spare time ;)
jonathandavid
 
Posts: 159

Re: Prompt class (please give your opinion)

Postby jonathandavid » Sun Jan 18, 2009 4:19 am

Chuck, I have a small feature request.

Could we have the possibility to give a name to the exception caught in a "expect" block? For this to be useful, the expect part should come after the main statement, and should be allowed its own block. For instance:

_
x as int? = nil
y = x to !
expect ex as InvalidOperationException
print ex



I would use this feature as part of the Prompt unit tests. I want to make sure that some code throws an exception, and I also want to make sure that the thrown exception is properly initialized (i.e., carries the desired error information). Of course, I can find an easy workaround usign a regular try/catch construct, so this is not urgent. Please note that, syntactically, implementing this feature would make it very easy to implement another desirable feature: shortcut exception catching.

_
x as int? = nil
y = x to !
catch ex as InvalidOperationException
print 'the conversion didn't work as expected'
Last edited by jonathandavid on Sun Jan 18, 2009 2:05 pm, edited 1 time in total.
jonathandavid
 
Posts: 159

Re: Prompt class (please give your opinion)

Postby jonathandavid » Sun Jan 18, 2009 1:18 pm

Another suggestion. I think Cobra should allow me to do the following:

class Prompt
enum ErrorCause
None,
EOF,
Cancel,
# etc.

pro errorCause from var as ErrorCause


Right now I can't because Cobra won't let me define two members that only differ in case (errorCause, ErrorCause). I agree that this might make sense if the members are of the same type (e.g., if I try to add two fields called errorCause and errorCAuse). But I don't see anything wrong with the example above, and I think it should be allowed. Right now Cobra is forcing me to rewrite the name of the enum from ErrorCause to Error, which in my opinion makes the code less readable. I think Cobra shouldn't stand in the way like this.


One more thing. Cobra does not seem to support the definition of inner classes. Will that change in the future, or is it a deliberate omission?
jonathandavid
 
Posts: 159

Re: Prompt class (please give your opinion)

Postby Charles » Mon Jan 19, 2009 3:33 am

jonathandavid wrote:Chuck, I have a small feature request.

Could we have the possibility to give a name to the exception caught in a "expect" block? For this to be useful, the expect part should come after the main statement, and should be allowed its own block. For instance:
...


Yeah, this has been on my mind. You want to be able to inspect the exception that was caught.
expect FooException, .myMethod

# how about:
expect fe as FooException, .myMethod
assert fe.foo == 3 and fe.bar > 2

This uses the same "<name> as <Type>" syntax as "catch". I don't see that it's desirable to push the "expect" down and in as proposed for the "catch", because unlike the "catch", you're expecting this as a regular thing.
Charles
 
Posts: 2510
Location: Los Angeles, CA

Re: Prompt class (please give your opinion)

Postby Charles » Mon Jan 19, 2009 3:34 am

jonathandavid wrote:Another suggestion. I think Cobra should allow me to do the following:
...
Right now I can't because Cobra won't let me define two members that only differ in case (errorCause, ErrorCause). I agree that this might make sense if the members are of the same type (e.g., if I try to add two fields called errorCause and errorCAuse). But I don't see anything wrong with the example above, and I think it should be allowed. Right now Cobra is forcing me to rewrite the name of the enum from ErrorCause to Error, which in my opinion makes the code less readable. I think Cobra shouldn't stand in the way like this.
...
One more thing. Cobra does not seem to support the definition of inner classes. Will that change in the future, or is it a deliberate omission?

I think in general it's a bad idea to have two identifiers in the same type that differ only in case, but mean different things. Furthermore, in the resulting .NET assembly, methods are capitalized so that they look normal to VB and C# developers that might use a library written in Cobra. So it can't be done anyway (without breaking that).

In my own projects, I often suffix the enum type with "Enum" which makes it clear in contrast to classes. Kind of like putting "I" before interfaces.

Regarding nested classes, they are just not implemented yet. I've made some provisions for them here and there in the compiler, but have not worked on them directly.
Charles
 
Posts: 2510
Location: Los Angeles, CA

Re: Prompt class (please give your opinion)

Postby jonathandavid » Mon Jan 19, 2009 4:08 am

Chuck wrote:# how about:

expect fe as FooException, .myMethod
assert fe.foo == 3 and fe.bar > 2


This uses the same "<name> as <Type>" syntax as "catch". I don't see that it's desirable to push the "expect" down and in as proposed for the "catch", because unlike the "catch", you're expecting this as a regular thing.



Yes that's probably better.

Wouldn't it be more readable to use "when" instead of ",":

expect fe as FooException when .myMethod
assert fe.foo == 3 and fe.bar > 2
jonathandavid
 
Posts: 159

Re: Prompt class (please give your opinion)

Postby jonathandavid » Mon Jan 19, 2009 9:41 am

I have two more issues to report. The first one is that type inference is probably not as intelligent as it could be, when delegates are involved. Consider:

class C

sig Bar

def f is shared
pass


def main is shared
foo = ref .f # does not compile "cannot infer type for "foo" because the type of the right hand expression is unknown"
foo2 as Bar = ref .f # compiles fine, because we're telling the compiler what the type should be


My point is that the first line of main should compile as well, and that the compiler should be wise enough to infer that the type for foo must be "delegate with no params and no return type".

The second issue occurs when I try to use a symbol that starts with int8:

class Foo
def main is shared
int8Variable = 0


The problem disappears if I change int8Variable to hint8Variable, but persists if I use float32Variable. So I guess it's a parser thing, the parser seems to be treating int8 and Variable as two separate tokens.
jonathandavid
 
Posts: 159

Next

Return to Discussion

Who is online

Users browsing this forum: No registered users and 6 guests

cron