Home > Archive > Functional > May 2007 > printf resisting to be used
You are viewing an archived Text-only version of the thread.
To view this thread in it's original format and/or if you want to reply to
this thread please [click here]
| Author |
printf resisting to be used
|
|
| Xcriber51 2007-04-25, 8:03 am |
| Hi
I've loaded the "Printf.hs" module, but when I try to use it, I keep
getting errors (no matter what I enter). Here's an example:
Text.Printf> printf "%.2f\n" 2.34567
ERROR - Unresolved overloading
*** Type : (Fractional a, PrintfArg a, PrintfType b) => b
*** Expression : printf "%.2f\n" 2.34567
What is going on?
<SHORT-RANT>Why is it always a hassle to use even the simplest things in
Haskell? Why is it so user-hostile? (At least to those with IQs not north
of 150?)</SHORT-RANT>
-- Ken
| |
| Phil Armstrong 2007-04-25, 8:03 am |
| Xcriber51 <xcriber@[omitted]> wrote:
> Hi
>
> I've loaded the "Printf.hs" module, but when I try to use it, I keep
> getting errors (no matter what I enter). Here's an example:
>
> Text.Printf> printf "%.2f\n" 2.34567
> ERROR - Unresolved overloading
> *** Type : (Fractional a, PrintfArg a, PrintfType b) => b
> *** Expression : printf "%.2f\n" 2.34567
>
> What is going on?
<Twiddles a bit> Ah, I see, you're loading Text/Printf.hs into the
hugs runtime? That does give the errors you're seeing.
You need to coerce the argument to a suitable type:
printf "%2f\n" (2.345::Float)
which will then give you another exciting error, because printf can
return either String or (IO a) and the runtime can't tell which you
mean. Coercing it to either works fine in this instance:
>(printf "%2f" (2.345::Float))::IO ()
2.345
>(printf "%2f" (2.345::Float))::String
"2.345"
ghci does better, in that the default coercions for numeric types
seems to allow the printf argument type to resolve, but the unresolved
return type for printf seems to lead to an uncaught exception:
Prelude Text.Printf> printf "%f\n" 2.345
2.345
*** Exception: Prelude.undefined
Prelude Text.Printf> (printf "%f\n" 2.345)::IO ()
2.345
Prelude Text.Printf> (printf "%f\n" 2.345)::String
"2.345\n"
> <SHORT-RANT>Why is it always a hassle to use even the simplest things in
> Haskell? Why is it so user-hostile? (At least to those with IQs not north
> of 150?)</SHORT-RANT>
I feel your pain (sometimes). But when you get it working, it's
lovely.
Phil
--
http://www.kantaka.co.uk/ .oOo. public key: http://www.kantaka.co.uk/gpg.txt
| |
| Barry Kelly 2007-04-25, 8:03 am |
| Phil Armstrong wrote:
> Xcriber51 <xcriber@[omitted]> wrote:
> 2.345
> "2.345"
>
>
> I feel your pain (sometimes). But when you get it working, it's
> lovely.
But surely you can't deny that the end result, i.e. the fixed code you
posted, is very far from lovely - in fact, it's hideous and redundant!
-- Barry
--
http://barrkel.blogspot.com/
| |
| Phil Armstrong 2007-04-25, 7:04 pm |
| Barry Kelly <barry.j.kelly@gmail.com> wrote:
> Phil Armstrong wrote:
>
>
> But surely you can't deny that the end result, i.e. the fixed code you
> posted, is very far from lovely - in fact, it's hideous and redundant!
As I understand things, you only need the type annotations to the
result of printf when trying to call it in the interpreter, where the
real-eval-print loop doesn't coerce the result type in the way that
using printf in an ordinary Haskell program would.
I don't mind the type annotations for the literal arguments
personally. They're more verbose that the C equivalents, but that's
not always a bad thing.
I won't argue with anyone who suggests that the type errors reported
by the interpreter are particularly confusing in this particular case
though.
Phil
--
http://www.kantaka.co.uk/ .oOo. public key: http://www.kantaka.co.uk/gpg.txt
| |
| Xcriber51 2007-04-25, 7:04 pm |
| OK, fine. Thanks, Phil.
Now here's my actual problem. Say I have this "interest" function:
-- pv : present value
-- nper: number of periods
-- ar : annual rate (of interest)
-- per : period
interest pv nper ar per = (pv*(nper-per)*ar)/(nper^2)
Say I use it as e.g.:
Main>sum [x | i <- [0..11], let x = interest 100 12 0.06 i]
3.25
Now, when I follow your suggestion and code this:
[x | i <- [0..11], let x = (printf "%2f\n" ((interest 100 12 0.06
i)::Float))::IO]
I get another sweet sweet error:
ERROR - Illegal type in type expression
I can't even fathom how illegal I've gotten by that time, so I just turn
around and start heading for the hills.
(Yes, yes, I know, it's perfectly sensible according to <insert your
PhD-worthy explanation here>. But, you see, life is short, and when I
study Haskell, I can almost feel the number of my brain cells dying by the
minute. I'm getting closer to 6-feet under.)
Ehm, OK, back to discussion. Another way of doing it is using "String"
since only it can "show" what. (What? What does "show" show, really?)
[x | i <- [0..11], let x = (printf "%.2f" ((interest 100 12 0.06
i)::Float))::String]
Yes, YES, Holly Moses, yippee. This does NOT issue an error. Haskell, this
is the kind of achievement that can make a grown-man-studying-Haskell weep.
The output is this:
["0.50","0.46","0.42","0.38","0.33","0.29","0.25","0.21","0.17","0.13","0.08","0.04"]
Trouble is, I can't "sum" this?
What do I do, Phil? Write a module that defines a sum in terms of IO (),
right? Or some other demented thing like that, I presume?
(Please bear in mind I'm only trying to be humorous, and not flaming
anyone.)
Your help will be much appreciated -- and delay my journey to 6-feet
under.
-- Ken
| |
| Xcriber51 2007-04-25, 7:04 pm |
| BTW I know it doesn't make any sense to send a series of numbers to IO ()
and then try to sum them, or even pass the numbers through printf and then
sum them when you can just, well, sum them. But -- big but -- as a
desperado in a rush, printf is the closest I could find that will give me
a series of numbers *rounded* to 2 decimals.
More importantly, I'd just like to see this discussed. Faster to learn
that way ;)
| |
| Chris Smith 2007-04-25, 7:04 pm |
| Xcriber51 <xcriber@[OMITTED]> wrote:
> Now, when I follow your suggestion and code this:
>
> [x | i <- [0..11], let x = (printf "%2f\n" ((interest 100 12 0.06
> i)::Float))::IO]
>
> I get another sweet sweet error:
>
> ERROR - Illegal type in type expression
The problem is that IO is not a simple type. IO is a type operator,
which creates types when it is applied to a type argument. If you
really want a list of IO actions, then change it to "IO ()" instead of
just "IO". What I suspect you wanted, though, is "String". Either one
works. In the former case, though, you have an array of IO actions,
which are not printable by themselves, so you have to do something like
this:
mapM_ id [x | i <- [0..11],
let x = (printf "%2f\n"
((interest 100 12 0.06 i)::Float))::IO]
--
Chris Smith
| |
| Dirk Thierbach 2007-04-25, 7:04 pm |
| Xcriber51 <xcriber@[omitted]> wrote:
> Now here's my actual problem. Say I have this "interest" function:
>
> -- pv : present value
> -- nper: number of periods
> -- ar : annual rate (of interest)
> -- per : period
>
> interest pv nper ar per = (pv*(nper-per)*ar)/(nper^2)
>
> Say I use it as e.g.:
>
> Main>sum [x | i <- [0..11], let x = interest 100 12 0.06 i]
> 3.25
>
> Now, when I follow your suggestion and code this:
>
> [x | i <- [0..11], let x = (printf "%2f\n" ((interest 100 12 0.06
> i)::Float))::IO]
Then what you want isn't to use printf, but what you really want
is to round the value to two digits after the comma. So, let's
write a function for that:
roundFracDigits :: RealFrac a => Int -> a -> a
roundFracDigits n x = fromInteger (round (x * k)) / k where k = 10 ^ n
How do I know I have to insert a "fromInteger" there? The type of
"round" is "(RealFrac a, Integral b) => a -> b", so it will convert a
floating point value to an integral value. Since I want to have a
floating point value in the end, I have to convert it back, and if I
pick fromInteger to do that, I also say to what integral value I want
to round to (namely, Integer, which is always a safe choice). Then
life is simple:
*Main> sum [x | i <- [0..11],
let x = roundFracDigits 2 $ interest 100 12 0.06 i]
3.25
The ($) is just there to avoid the parenthesis (you'll see this style
quite often).
> But, you see, life is short, and when I study Haskell, I can almost
> feel the number of my brain cells dying by the minute.
Maybe the following simple rules help:
- Think typed. Mentally attach a type to everything you use.
"This is a String, that there is an Integer", etc.
- If you want to calculate, you need numbers, not strings.
- To convert values to a string, avoid printf. Use show instead. If
you don't care about efficiency, just concatenate the results with (++),
as in "The interest is " ++ show $ interest 100 12 0.06 5 ++ "."
That's easiest to understand. Next, take a look at "shows".
- If you get lost in the various numeric classes, look at the types of
the functions, and consider inserting a conversion.
> What does "show" show, really?
"show" converts any value to a String (well, those values where
are instances of the type class Show, so the compiler knows how to
do it.) The inverse function (more or less) is "read", which
converts a string to any value.
- Dirk
| |
| Phil Armstrong 2007-04-25, 7:04 pm |
| Xcriber51 <xcriber@[omitted]> wrote:
> Now here's my actual problem. Say I have this "interest" function:
>
> -- pv : present value
> -- nper: number of periods
> -- ar : annual rate (of interest)
> -- per : period
>
> interest pv nper ar per = (pv*(nper-per)*ar)/(nper^2)
>
> Say I use it as e.g.:
>
> Main>sum [x | i <- [0..11], let x = interest 100 12 0.06 i]
> 3.25
>
> Now, when I follow your suggestion and code this:
>
> [x | i <- [0..11], let x = (printf "%2f\n" ((interest 100 12 0.06
> i)::Float))::IO]
>
> I get another sweet sweet error:
>
> ERROR - Illegal type in type expression
>
> I can't even fathom how illegal I've gotten by that time, so I just turn
> around and start heading for the hills.
I can see how you might choose to do that :)
OK. Your problem here is that IO is not a type, it's a type
constructor. If you go back and look at my original reply, you'll see
that I used the type "IO ()", which you could read as IO (null
type). This is the same type that main has in a haskell program.
> (Yes, yes, I know, it's perfectly sensible according to <insert your
> PhD-worthy explanation here>. But, you see, life is short, and when I
> study Haskell, I can almost feel the number of my brain cells dying by the
> minute. I'm getting closer to 6-feet under.)
I don't actually have a PhD as it happens :)
> Ehm, OK, back to discussion. Another way of doing it is using "String"
> since only it can "show" what. (What? What does "show" show, really?)
Hugs.Base> :t show
show :: Show a => a -> String
IOW, show is a polymorphic function that you can define on an
arbitrary type to allow it to be turned into a string (usually for
printing purposes). If a type doesn't have a show function, then it
can't be printed by any of the standard Haskell output functions,
which operate by using show (OK, probably some of the related
functions, but we'll gloss over that detail).
> [x | i <- [0..11], let x = (printf "%.2f" ((interest 100 12 0.06
> i)::Float))::String]
>
> Yes, YES, Holly Moses, yippee. This does NOT issue an error. Haskell, this
> is the kind of achievement that can make a grown-man-studying-Haskell weep.
> The output is this:
>
> ["0.50","0.46","0.42","0.38","0.33","0.29","0.25","0.21","0.17","0.13","0.08","0.04"]
>
> Trouble is, I can't "sum" this?
No surprise there! See below...
> What do I do, Phil? Write a module that defines a sum in terms of IO (),
> right? Or some other demented thing like that, I presume?
> (Please bear in mind I'm only trying to be humorous, and not flaming
> anyone.)
Don't worry, I'm certainly not offended!
> Your help will be much appreciated -- and delay my journey to 6-feet
> under.
OK. A couple of suggestions. Firstly, you can eliminate the coercion
to Float in the printf argument by making the type of interest
explicit:
interest :: Float -> Int -> Float -> Int -> Float
perhaps. This is often a good idea, even for little helper functions
as it means that any type errors reported will be more readable,
otherwise you get the result of coercing the type of the 'interest'
function to some weird type as a result of an error elsewhere and
things can get very strange...
OK, now you want to truncate the values output by your arithmetic to 2
significant figures. As you've worked out, one way to do this is with
printf, but your problem is that printf either returns thunks that
represent the action of printing the strings (values in the IO Monad),
or a bunch of strings. You don't want the IO values, because you can't
do anything with them except invoke them, so you need the strings. How
do you get the numbers back? Well, you need to parse them, so drag in
the read function, plus some more type coercion, and the magic of
foldr & map:
foldr (+) 0 (map read ([x | i <- [0..11], let x = (printf "%2f"
((interest 100 12 0.06 i)::Float))::String])::[Float])
By this point, you're probably thinking that this is barking up the
wrong tree & I'd agree. Converting numbers to Strings and back is
doing and end-run around the type system after all!
A cleaner way might be to either use some kind of fixed-point
arithmetic in the first place, or to use rational numbers rounded with
with approxRational.
cheers, Phil
--
http://www.kantaka.co.uk/ .oOo. public key: http://www.kantaka.co.uk/gpg.txt
| |
| Chris Smith 2007-04-25, 7:04 pm |
| Xcriber51 <xcriber@[OMITTED]> wrote:
> The output is this:
>
> ["0.50","0.46","0.42","0.38","0.33","0.29","0.25","0.21","0.17","0.13","0.08","0.04"]
>
> Trouble is, I can't "sum" this?
(I missed this part the first time around.)
Doing things this way is a really bad idea, so see Dirk's response.
Just as a random aside, though, if you wanted to sum this:
Prelude> sum (map read ["0.50", "0.46", "0.42", "0.38", "0.33", "0.29",
"0.25", "0.21", "0.17", "0.13", "0.08", "0.04"] :: [Float])
3.2599998
You can clearly see the accumulating error from floating-point
arithmetic here, so this does demonstrate why rounding this way is a
poor idea, as well.
--
Chris Smith
| |
| Xcriber51 2007-04-25, 7:04 pm |
| Thanks, everyone. I'm learning quite a bit from your examples.
As I've said earlier in this thread, I know that what I'm doing is a "poor
idea" (diplomatic way of saying it's silly ;-)), but, again, as I've
mentioned in a previous thread, that's the way some of us learn: by trial
-- of the darnest things -- and error.
The simplest and the only sensible solution is naturally Dirk's, but to
come up with that (i.e. to say "You wanna round to 2 decimal digits?
Multiply by 100, round it, then divide it by 100, you doofus!") you need a
mathematically disciplined mind -- which, ehm, is not the forte of yours
truly.
Thanks again.
-- Ken
| |
| Dirk Thierbach 2007-04-26, 4:05 am |
| Xcriber51 <xcriber@[omitted]> wrote:
> As I've said earlier in this thread, I know that what I'm doing is a "poor
> idea" (diplomatic way of saying it's silly ;-)), but, again, as I've
> mentioned in a previous thread, that's the way some of us learn: by trial
> -- of the darnest things -- and error.
Yes, that's how we all learn :-) However, as you have probably noticed,
Haskell is really unforgiving if you don't pay attention to types.
You need to make sure that the square peg goes into the square
hole and the round peg into the round hole. Otherwise you'll get
frustrated to no end as you try to put pegs into the wrong hole, and
they won't fit, no matter how you turn them :-)
Here's a couple of things I thought may also help:
1) Always write down type signatures. That's the easiest way to enforce
you get used to thinking about types. What I usually do is to write
down the function with the arguments first, say
interest pv nper ar per =
Then I think about the types, so I'd write above that for example
interest :: Double -> Integer -> Double -> Integer -> Double
(At this stage, I would probably swap nper and ar to make the type
signature nicer and group arguments together that belong together.)
Then I start writing the function. Let's say I get as far as
interest pv nper ar per = pv*(nper-per)
Here I notice that nper and per are Integers, while pv is Double.
That doesn't work, I have to insert "fromInteger":
interest pv nper ar per = pv * fromInteger (nper-per)
Then I go on to complete the function in a similar way:
interest pv nper ar per =
pv * fromInteger (nper-per) * ar / (fromInteger nper)^2
2) If you want to play around, then uncomment your type signature and
compare it with the type signature the compiler infers, which is often
more general:
interest :: (Fractional a) => a -> Integer -> a -> Integer -> a
So you see the compiler says that you can actually use your function
with any fractional type if you want, not only with Double. If you'd change
the "fromInteger" above to "fromIntegral", then you could use any integral
type for the period arguments (not that this would make a lot of sense here).
3) BTW, here's a quick overview of most of the numeric classes:
Num - anything that supports addition, subtraction, and multiplication.
That includes all numbers, but also things like matrices (though you'd
have to define instances for that.) The mathematical name for that
is "ring".
Integral - any Num type that additionally supports division with
remainder. That includes more unexptected things like polynomials.
In maths talk, that's an "euclidean ring".
Fractional - any Num type that has ordinary division. Float, Double,
Rational and Complex are examples. The maths name is "field".
RealFrac - any "real valued" Fractional which you can round and truncate.
Floating - any Fractional that supports trigonometric functions, sqrt,
logarithms etc.
It's like object orient programming -- classes are defined by the
functions they support.
4) If you get by all those numeric classes, just use Integer
and Double everywhere.
> The simplest and the only sensible solution is naturally Dirk's, but to
> come up with that (i.e. to say "You wanna round to 2 decimal digits?
> Multiply by 100, round it, then divide it by 100, you doofus!") you need a
> mathematically disciplined mind -- which, ehm, is not the forte of yours
> truly.
It helps of course if you have used this before :-) (In my case, that was
IIRC in some early BASIC). And it's always important to clearly state the
problem before you attempt a solution. Once you really know what the
problem is, it becomes often obvious what to do.
And actually, there are other solutions. I am still not sure what you
really want to do, because as has been already pointed out, early
rounding introduces errors when sticking to floating point
numbers. Maybe what you want is fixed point numbers, that do rounding
for each operation. One could declare a datatype of its own for that,
which implement it as a "scaled" Integer. Another option would be to
use Rational, so after the rounding you can carry out other
operations, even multiplications etc., without any errors. Here's how
you'd do that (the operator (%) constructs a Rational from two
Integers, e.g. 1 % 4 is one quarter):
import Data.Ratio
roundDigitsRational :: RealFrac a => Int -> a -> Rational
roundDigitsRational n x = round (x * k) % k where k = 10 ^ n
(Exercise: The function as written above doesn't work, you have to
insert a conversion function. Which one, and where? Don't rely on the
error message, it doesn't help too much. Go through the subexpressions,
and determine the types.)
(Exercise: Actually, (%) is a bit more flexible. Look at its type,
and try to figure out what else you could do with it.)
One di vantage of this approach is that Rationals are converted
by show to "x % y" form, so you'll have to write another function
to convert it to "xxxx.xx", if you need that.
- Dirk
| |
| Xcriber51 2007-04-26, 4:05 am |
| Beautiful, Dirk. Thanks for taking the time to write all this. Much
appreciated.
I'll print this whole thread and study it.
Many thanks, everyone!
-- Ken
| |
| Chris F Clark 2007-04-26, 7:04 pm |
| I want to 2nd the thanks (to everyone). I got at least 2 posts from
this thread that I've added to my saved collection of things I should
learn.
| |
| Albert Y. C. Lai 2007-04-26, 7:04 pm |
| (printf "%d\n" x) could be a string (pure value) or an output action
(side-effecting). The "x" there could be Int or Integer or...
If you take it out of context and utter it at a standalone prompt, of
course it's ambiguous. But so is any real language: "China" uttered
without context could mean a country or a ceramic or...
In the Haskell case the ambiguity is for a good cause: generality. Plug
that code into a string context and it becomes a string --- no extra
type annotation. Plug the same code into an IO context and it becomes an
output action --- no extra type annotation. If you translate this code
to Perl, you have to change some of them to "sprintf" and keep some
others as "printf". And you would think Perl was the standard for
ambiguous, context-sensitive languages...
(Similarly for the ambiguity of x.)
This is a very powerful tool. The question is why is such a powerful
tool so difficult to learn and use. But then, phrased that way, it
becomes quite tautological, isn't it?
(Aside: Many people run into difficulties translating other languages to
Haskell. Some of the difficulties raise good questions, but others don't
do justice. Doing the reverse translation provides a refreshing
perspective. Hence, I think a good way to understand Haskell is "C for
Haskell programmers", "Perl for Haskell programmers", etc.)
| |
| David Hopwood 2007-04-27, 10:03 pm |
| Barry Kelly wrote:
> Phil Armstrong wrote:
>
>
> But surely you can't deny that the end result, i.e. the fixed code you
> posted, is very far from lovely - in fact, it's hideous and redundant!
In this case, the design of the printf module is responsible. Overloading
on the argument type is reasonable, but the IO () vs String overload on the
return type is not. If the intent is to follow C, the latter should have
been called sprintf.
--
David Hopwood <david.hopwood@industrial-designers.co.uk>
| |
| David Hopwood 2007-04-27, 10:03 pm |
| Xcriber51 wrote:
> Thanks, everyone. I'm learning quite a bit from your examples.
>
> As I've said earlier in this thread, I know that what I'm doing is a "poor
> idea" (diplomatic way of saying it's silly ;-)), but, again, as I've
> mentioned in a previous thread, that's the way some of us learn: by trial
> -- of the darnest things -- and error.
>
> The simplest and the only sensible solution is naturally Dirk's, but to
> come up with that (i.e. to say "You wanna round to 2 decimal digits?
> Multiply by 100, round it, then divide it by 100, you doofus!") you need a
> mathematically disciplined mind -- which, ehm, is not the forte of yours
> truly.
If you want to sum a list of numbers rounded to 2 d.p., then the easiest
correct way to do it is to multiply each element by 100, round each element
to an integer, sum, *then* divide by 100. And if this is used in a larger
problem where everything is rounded to 2 d.p., consider using integers
throughout except for I/O.
--
David Hopwood <david.hopwood@industrial-designers.co.uk>
| |
| Dirk Thierbach 2007-04-28, 4:07 am |
| David Hopwood <david.hopwood@industrial-designers.co.uk> wrote:
> If you want to sum a list of numbers rounded to 2 d.p., then the easiest
> correct way to do it is to multiply each element by 100, round each element
> to an integer, sum, *then* divide by 100.
That's actually the same as using a "scaled Integer" (= fixed point)
datatype.
> And if this is used in a larger problem where everything is rounded
> to 2 d.p., consider using integers throughout except for I/O.
And especially in this case it makes a lot of sense to introduce
a new datatype for it, otherwise it's easy to confuse normal Integers
and scaled Integers after a long calculation.
It may even make sense to introduce a datatype for "money", disallow
multiplication for it, but make a new operator that allows
multiplication of normal Integers and "money".
It all depends on what you want to do, and if the problem is large enough
so that the extra overhead pays off.
- Dirk
| |
| Phil Armstrong 2007-04-28, 8:03 am |
| Organization: Disorganisation
User-Agent: tin/1.9.2-20070201 ("Dalaruan") (UNIX) (Linux/2.6.18-4-k7 (i686))
Date: Sat, 28 Apr 2007 12:11:25 +0100
Lines: 17
NNTP-Posting-Host: 217.155.153.11
X-Trace: 1177762101 news.gradwell.net 644 fsel/217.155.153.11:40053
X-Complaints-To: news-abuse@gradwell.net
Xref: number1.nntp.dca.giganews.com comp.lang.functional:60962
Dirk Thierbach <dthierbach@usenet.arcornews.de> wrote:
> It all depends on what you want to do, and if the problem is large enough
> so that the extra overhead pays off.
The great thing about Haskell[1] is that the extra overhead for a new
type like that is about as minimal as it gets.
For anything larger than a use-once knock off script it would probably
repay the effort in better code.
Phil
[1] Since this is comp.lang.functional: s/Haskell/a language with a
decent type system like Haskell's/
--
http://www.kantaka.co.uk/ .oOo. public key: http://www.kantaka.co.uk/gpg.txt
| |
| Phil Armstrong 2007-04-30, 10:04 pm |
| Phil Armstrong <phil-news@kantaka.co.uk> wrote:
> Dirk Thierbach <dthierbach@usenet.arcornews.de> wrote:
>
> The great thing about Haskell[1] is that the extra overhead for a new
> type like that is about as minimal as it gets.
>
> For anything larger than a use-once knock off script it would probably
> repay the effort in better code.
And inevitably, someone has blogged some suitable code using Rational
numbers, a given epsilon for the accuracy and the magic of
approxRational:
http://augustss.blogspot.com/2007/0...-i-was-bit.html
I wonder if Lennert read this thread?
Phil
--
http://www.kantaka.co.uk/ .oOo. public key: http://www.kantaka.co.uk/gpg.txt
| |
|
|
|
|
|