3 ms·
> More seriously, ask the question: what relationship between multiple values is useful for returning from functions but not useful enough to be a data structur
by kerkeslager 13y ago
> More seriously, ask the question: what relationship between multiple values is useful for returning from functions but not useful enough to be a data structure that can be used in other parts of code?
Sometimes there are pieces of data which are hard to create a cohesive type around. That is, you can compose an new type from other types, but you can't really add any behavior to that new type. This is exemplified by the Tuple type in many languages: it's basically a hack to treat disparate pieces of data as one, and it doesn't offer any way to operate on its own data.
Here's an example from a relatively typical idiom of parsing an int from a string:
int parsedInt;
if(int.TryParse(s, out parsedInt))
{
Console.WriteLine("Parsed {0}",parsedInt);
}
else
{
Console.WriteLine("{0} was not an integer",s);
}
The problem here is that TryParse() really returns two values, one of which will be tested to see if the parse was successful, and the other of which is assigned to parseInt. You could test that the string can be parsed and then get the parsed value in two separate functions, but this would be inefficient because you'd essentially have to parse the string twice. I'm on my Mac so I'm not sure I'm remembering this correctly, but I think C# forces you to put the "out" there, which I think is good because it clarifies that parsedInt will be assigned. However, I think there are a number of ways in which the following (hypothetical) redefinition of TryParse (and C# syntax) feels a lot clearer:
var parseSucceeded, parsedInt = int.TryParse(s);
if(parseSucceeded)
{
Console.WriteLine("Parsed {0}",parsedInt);
}
else
{
Console.WriteLine("{0} was not an integer",s);
}
First of all, the programmer has been forced to assign the success boolean to a clarifying variable. Second, the call has a clearer structure: the input are on the right side of the = and the outputs are on the left.
Let's look at some other possible solutions here:
var parseTuple = int.TryParse(s);
if(parseTuple.Item1)
{
Console.WriteLine("Parsed {0}",parseTuple.Item2);
}
else
{
Console.WriteLine("{0} was not an integer",s);
}
This results in some confusing code. Item1 and Item2 are very poor names for the things they represent.
IntegerParseResult parseResult = int.TryParse(s); // Didn't use var here to clarify what this code means.
if(parseResult.ParseSucceeded)
{
Console.WriteLine("Parsed {0}",parseTuple.ParsedInt);
}
else
{
Console.WriteLine("{0} was not an integer",s);
}
This is a little better, but the creation of a whole new type seems heavyweight, especially given that this new type provides no real functionality. It's tying together two pieces of data which are about to be separated and never used together again (consider, for example, that instead of printing to the console it might return).
I guess the argument I'm making here is that just because a function returns to outputs doesn't mean those outputs have a meaningful relationship that would justify having a new type.
- derleth 13y agoI think that's a bad example, because your TryParse function can be thought of as returning a value wrapped in a monad. A Maybe monad, to be specific. In Haskell: case (tryParse s) of Just n -> putStrLn "Parsed " ++ (show n) Nothing -> putStrLn (s ++ " was not an integer") Nothing is a very convenient little concept: It's a value which isn't a 'legitimate' value in any context. It's always a flag value. In C, for example, there's no way to have a flag value of type int in this context, because tryParse could conceivably return any value of type int as its legitimate result. It's also wonderfully conceptually clean: The function either just returns a legitimate value or it returns nothing legitimate at all, and you always know which it did. The point is, however, it only returns one value, and doesn't rely on any out-of-band communications through errno or similar. If you want to get fancy, you can use the Either monad. This can allow you to say why a parse failed, for example: case (tryParse s) of Left n -> putStrLn "Parsed " ++ (show n) Right m -> putStrLn "Parse of " ++ s ++ " failed because " ++ m In this case, tryParse would have the type: tryParse :: String -> Either Int String The point here is, you still only return one value, but they can be either of two different kinds of value, as distinguished by being tagged either Left or Right, and can even be different types. Again, one value, no out-of-band signalling. And, of course, Haskell has tuples. You can do interesting things without them, however.
- ufmace 13y agoC# has kind of an equivalent of that, at least for this purpose, of nullable value types. Thus, you could do: int? result = int.TryParse(s); if(result.HasValue) { Console.WriteLine("Parsed {0}",result.Value); } else { Console.WriteLine("{0} was not an integer",s); } And in C#, you could do the second kinda like: object result = int.TryParse(s); if(result is int) { Console.WriteLine("Parsed {0}",result); } else { Console.WriteLine("{0} was not an integer",s); } Though there's nowhere simple to make it clear that this TryParse either returns the parsed int, or a string describing why the parse failed. And returning object kinda goes against the spirit of the language, IMHO.
- asolove 13y agoThis is the outstanding example of how elevating special function return data into a type adds power, because you've just described a monad.